Showing
17 changed files
with
275 additions
and
35 deletions
app/Http/Controllers/SettingController.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +namespace Oxbow\Http\Controllers; | ||
| 4 | + | ||
| 5 | +use Illuminate\Http\Request; | ||
| 6 | + | ||
| 7 | +use Oxbow\Http\Requests; | ||
| 8 | +use Oxbow\Http\Controllers\Controller; | ||
| 9 | +use Setting; | ||
| 10 | + | ||
| 11 | +class SettingController extends Controller | ||
| 12 | +{ | ||
| 13 | + /** | ||
| 14 | + * Display a listing of the settings. | ||
| 15 | + * | ||
| 16 | + * @return Response | ||
| 17 | + */ | ||
| 18 | + public function index() | ||
| 19 | + { | ||
| 20 | + $this->checkPermission('settings-update'); | ||
| 21 | + return view('settings/index'); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * Update the specified settings in storage. | ||
| 28 | + * | ||
| 29 | + * @param Request $request | ||
| 30 | + * @return Response | ||
| 31 | + */ | ||
| 32 | + public function update(Request $request) | ||
| 33 | + { | ||
| 34 | + $this->checkPermission('settings-update'); | ||
| 35 | + // Cycles through posted settings and update them | ||
| 36 | + foreach($request->all() as $name => $value) { | ||
| 37 | + if(strpos($name, 'setting-') !== 0) continue; | ||
| 38 | + $key = str_replace('setting-', '', trim($name)); | ||
| 39 | + Setting::put($key, $value); | ||
| 40 | + } | ||
| 41 | + return redirect('/settings'); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | +} |
| ... | @@ -71,6 +71,10 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -71,6 +71,10 @@ Route::group(['middleware' => 'auth'], function () { |
| 71 | Route::get('/', 'HomeController@index'); | 71 | Route::get('/', 'HomeController@index'); |
| 72 | Route::get('/home', 'HomeController@index'); | 72 | Route::get('/home', 'HomeController@index'); |
| 73 | 73 | ||
| 74 | + // Settings | ||
| 75 | + Route::get('/settings', 'SettingController@index'); | ||
| 76 | + Route::post('/settings', 'SettingController@update'); | ||
| 77 | + | ||
| 74 | 78 | ||
| 75 | }); | 79 | }); |
| 76 | 80 | ... | ... |
| ... | @@ -4,6 +4,7 @@ namespace Oxbow\Providers; | ... | @@ -4,6 +4,7 @@ namespace Oxbow\Providers; |
| 4 | 4 | ||
| 5 | use Illuminate\Support\ServiceProvider; | 5 | use Illuminate\Support\ServiceProvider; |
| 6 | use Oxbow\Services\ActivityService; | 6 | use Oxbow\Services\ActivityService; |
| 7 | +use Oxbow\Services\SettingService; | ||
| 7 | 8 | ||
| 8 | class CustomFacadeProvider extends ServiceProvider | 9 | class CustomFacadeProvider extends ServiceProvider |
| 9 | { | 10 | { |
| ... | @@ -27,5 +28,9 @@ class CustomFacadeProvider extends ServiceProvider | ... | @@ -27,5 +28,9 @@ class CustomFacadeProvider extends ServiceProvider |
| 27 | $this->app->bind('activity', function() { | 28 | $this->app->bind('activity', function() { |
| 28 | return new ActivityService($this->app->make('Oxbow\Activity')); | 29 | return new ActivityService($this->app->make('Oxbow\Activity')); |
| 29 | }); | 30 | }); |
| 31 | + | ||
| 32 | + $this->app->bind('setting', function() { | ||
| 33 | + return new SettingService($this->app->make('Oxbow\Setting')); | ||
| 34 | + }); | ||
| 30 | } | 35 | } |
| 31 | } | 36 | } | ... | ... |
| ... | @@ -81,11 +81,13 @@ class ActivityService | ... | @@ -81,11 +81,13 @@ class ActivityService |
| 81 | * Gets the latest activity. | 81 | * Gets the latest activity. |
| 82 | * @param int $count | 82 | * @param int $count |
| 83 | * @param int $page | 83 | * @param int $page |
| 84 | + * @return array | ||
| 84 | */ | 85 | */ |
| 85 | public function latest($count = 20, $page = 0) | 86 | public function latest($count = 20, $page = 0) |
| 86 | { | 87 | { |
| 87 | - return $this->activity->orderBy('created_at', 'desc') | 88 | + $activityList = $this->activity->orderBy('created_at', 'desc') |
| 88 | ->skip($count * $page)->take($count)->get(); | 89 | ->skip($count * $page)->take($count)->get(); |
| 90 | + return $this->filterSimilar($activityList); | ||
| 89 | } | 91 | } |
| 90 | 92 | ||
| 91 | /** | 93 | /** |
| ... | @@ -99,7 +101,7 @@ class ActivityService | ... | @@ -99,7 +101,7 @@ class ActivityService |
| 99 | function entityActivity($entity, $count = 20, $page = 0) | 101 | function entityActivity($entity, $count = 20, $page = 0) |
| 100 | { | 102 | { |
| 101 | $activity = $entity->hasMany('Oxbow\Activity')->orderBy('created_at', 'desc') | 103 | $activity = $entity->hasMany('Oxbow\Activity')->orderBy('created_at', 'desc') |
| 102 | - ->skip($count*$page)->take($count)->get(); | 104 | + ->skip($count * $page)->take($count)->get(); |
| 103 | 105 | ||
| 104 | return $this->filterSimilar($activity); | 106 | return $this->filterSimilar($activity); |
| 105 | } | 107 | } |
| ... | @@ -109,16 +111,17 @@ class ActivityService | ... | @@ -109,16 +111,17 @@ class ActivityService |
| 109 | * @param Activity[] $activity | 111 | * @param Activity[] $activity |
| 110 | * @return array | 112 | * @return array |
| 111 | */ | 113 | */ |
| 112 | - protected function filterSimilar($activity) { | 114 | + protected function filterSimilar($activity) |
| 115 | + { | ||
| 113 | $newActivity = []; | 116 | $newActivity = []; |
| 114 | $previousItem = false; | 117 | $previousItem = false; |
| 115 | - foreach($activity as $activityItem) { | 118 | + foreach ($activity as $activityItem) { |
| 116 | - if($previousItem === false) { | 119 | + if ($previousItem === false) { |
| 117 | $previousItem = $activityItem; | 120 | $previousItem = $activityItem; |
| 118 | $newActivity[] = $activityItem; | 121 | $newActivity[] = $activityItem; |
| 119 | continue; | 122 | continue; |
| 120 | } | 123 | } |
| 121 | - if(!$activityItem->isSimilarTo($previousItem)) { | 124 | + if (!$activityItem->isSimilarTo($previousItem)) { |
| 122 | $newActivity[] = $activityItem; | 125 | $newActivity[] = $activityItem; |
| 123 | } | 126 | } |
| 124 | $previousItem = $activityItem; | 127 | $previousItem = $activityItem; | ... | ... |
app/Services/Facades/Setting.php
0 → 100644
| 1 | +<?php namespace Oxbow\Services\Facades; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +use Illuminate\Support\Facades\Facade; | ||
| 5 | + | ||
| 6 | +class Setting extends Facade | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Get the registered name of the component. | ||
| 10 | + * | ||
| 11 | + * @return string | ||
| 12 | + */ | ||
| 13 | + protected static function getFacadeAccessor() { return 'setting'; } | ||
| 14 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
app/Services/SettingService.php
0 → 100644
| 1 | +<?php namespace Oxbow\Services; | ||
| 2 | + | ||
| 3 | +use Oxbow\Setting; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * Class SettingService | ||
| 7 | + * | ||
| 8 | + * The settings are a simple key-value database store. | ||
| 9 | + * | ||
| 10 | + * @package Oxbow\Services | ||
| 11 | + */ | ||
| 12 | +class SettingService | ||
| 13 | +{ | ||
| 14 | + | ||
| 15 | + protected $setting; | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * SettingService constructor. | ||
| 19 | + * @param $setting | ||
| 20 | + */ | ||
| 21 | + public function __construct(Setting $setting) | ||
| 22 | + { | ||
| 23 | + $this->setting = $setting; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * Gets a setting from the database, | ||
| 28 | + * If not found, Returns default, Which is false by default. | ||
| 29 | + * @param $key | ||
| 30 | + * @param string|bool $default | ||
| 31 | + * @return bool|string | ||
| 32 | + */ | ||
| 33 | + public function get($key, $default = false) | ||
| 34 | + { | ||
| 35 | + $setting = $this->getSettingObjectByKey($key); | ||
| 36 | + return $setting === null ? $default : $setting->value; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * Checks if a setting exists. | ||
| 41 | + * @param $key | ||
| 42 | + * @return bool | ||
| 43 | + */ | ||
| 44 | + public function has($key) | ||
| 45 | + { | ||
| 46 | + $setting = $this->getSettingObjectByKey($key); | ||
| 47 | + return $setting !== null; | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * Add a setting to the database. | ||
| 52 | + * @param $key | ||
| 53 | + * @param $value | ||
| 54 | + * @return bool | ||
| 55 | + */ | ||
| 56 | + public function put($key, $value) | ||
| 57 | + { | ||
| 58 | + $setting = $this->setting->firstOrNew([ | ||
| 59 | + 'setting_key' => $key | ||
| 60 | + ]); | ||
| 61 | + $setting->value = $value; | ||
| 62 | + $setting->save(); | ||
| 63 | + return true; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * Removes a setting from the database. | ||
| 68 | + * @param $key | ||
| 69 | + * @return bool | ||
| 70 | + */ | ||
| 71 | + public function remove($key) | ||
| 72 | + { | ||
| 73 | + $setting = $this->getSettingObjectByKey($key); | ||
| 74 | + if($setting) { | ||
| 75 | + $setting->delete(); | ||
| 76 | + } | ||
| 77 | + return true; | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * Gets a setting model from the database for the given key. | ||
| 82 | + * @param $key | ||
| 83 | + * @return mixed | ||
| 84 | + */ | ||
| 85 | + private function getSettingObjectByKey($key) { | ||
| 86 | + return $this->setting->where('setting_key', '=', $key)->first(); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
app/Setting.php
0 → 100644
| ... | @@ -13,7 +13,7 @@ return [ | ... | @@ -13,7 +13,7 @@ return [ |
| 13 | | | 13 | | |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | - 'debug' => env('APP_DEBUG', false), | 16 | + 'debug' => env('APP_DEBUG', false), |
| 17 | 17 | ||
| 18 | /* | 18 | /* |
| 19 | |-------------------------------------------------------------------------- | 19 | |-------------------------------------------------------------------------- |
| ... | @@ -26,7 +26,7 @@ return [ | ... | @@ -26,7 +26,7 @@ return [ |
| 26 | | | 26 | | |
| 27 | */ | 27 | */ |
| 28 | 28 | ||
| 29 | - 'url' => 'http://localhost', | 29 | + 'url' => 'http://localhost', |
| 30 | 30 | ||
| 31 | /* | 31 | /* |
| 32 | |-------------------------------------------------------------------------- | 32 | |-------------------------------------------------------------------------- |
| ... | @@ -39,7 +39,7 @@ return [ | ... | @@ -39,7 +39,7 @@ return [ |
| 39 | | | 39 | | |
| 40 | */ | 40 | */ |
| 41 | 41 | ||
| 42 | - 'timezone' => 'UTC', | 42 | + 'timezone' => 'UTC', |
| 43 | 43 | ||
| 44 | /* | 44 | /* |
| 45 | |-------------------------------------------------------------------------- | 45 | |-------------------------------------------------------------------------- |
| ... | @@ -52,7 +52,7 @@ return [ | ... | @@ -52,7 +52,7 @@ return [ |
| 52 | | | 52 | | |
| 53 | */ | 53 | */ |
| 54 | 54 | ||
| 55 | - 'locale' => 'en', | 55 | + 'locale' => 'en', |
| 56 | 56 | ||
| 57 | /* | 57 | /* |
| 58 | |-------------------------------------------------------------------------- | 58 | |-------------------------------------------------------------------------- |
| ... | @@ -78,9 +78,9 @@ return [ | ... | @@ -78,9 +78,9 @@ return [ |
| 78 | | | 78 | | |
| 79 | */ | 79 | */ |
| 80 | 80 | ||
| 81 | - 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), | 81 | + 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), |
| 82 | 82 | ||
| 83 | - 'cipher' => 'AES-256-CBC', | 83 | + 'cipher' => 'AES-256-CBC', |
| 84 | 84 | ||
| 85 | /* | 85 | /* |
| 86 | |-------------------------------------------------------------------------- | 86 | |-------------------------------------------------------------------------- |
| ... | @@ -95,7 +95,7 @@ return [ | ... | @@ -95,7 +95,7 @@ return [ |
| 95 | | | 95 | | |
| 96 | */ | 96 | */ |
| 97 | 97 | ||
| 98 | - 'log' => 'single', | 98 | + 'log' => 'single', |
| 99 | 99 | ||
| 100 | /* | 100 | /* |
| 101 | |-------------------------------------------------------------------------- | 101 | |-------------------------------------------------------------------------- |
| ... | @@ -108,7 +108,7 @@ return [ | ... | @@ -108,7 +108,7 @@ return [ |
| 108 | | | 108 | | |
| 109 | */ | 109 | */ |
| 110 | 110 | ||
| 111 | - 'providers' => [ | 111 | + 'providers' => [ |
| 112 | 112 | ||
| 113 | /* | 113 | /* |
| 114 | * Laravel Framework Service Providers... | 114 | * Laravel Framework Service Providers... |
| ... | @@ -165,7 +165,7 @@ return [ | ... | @@ -165,7 +165,7 @@ return [ |
| 165 | | | 165 | | |
| 166 | */ | 166 | */ |
| 167 | 167 | ||
| 168 | - 'aliases' => [ | 168 | + 'aliases' => [ |
| 169 | 169 | ||
| 170 | 'App' => Illuminate\Support\Facades\App::class, | 170 | 'App' => Illuminate\Support\Facades\App::class, |
| 171 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, | 171 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, |
| ... | @@ -210,7 +210,8 @@ return [ | ... | @@ -210,7 +210,8 @@ return [ |
| 210 | * Custom | 210 | * Custom |
| 211 | */ | 211 | */ |
| 212 | 212 | ||
| 213 | - 'Activity' => Oxbow\Services\Facades\Activity::class, | 213 | + 'Activity' => Oxbow\Services\Facades\Activity::class, |
| 214 | + 'Setting' => Oxbow\Services\Facades\Setting::class, | ||
| 214 | 215 | ||
| 215 | ], | 216 | ], |
| 216 | 217 | ... | ... |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Database\Schema\Blueprint; | ||
| 4 | +use Illuminate\Database\Migrations\Migration; | ||
| 5 | + | ||
| 6 | +class CreateSettingsTable extends Migration | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Run the migrations. | ||
| 10 | + * | ||
| 11 | + * @return void | ||
| 12 | + */ | ||
| 13 | + public function up() | ||
| 14 | + { | ||
| 15 | + Schema::create('settings', function (Blueprint $table) { | ||
| 16 | + $table->string('setting_key')->primary()->indexed(); | ||
| 17 | + $table->text('value'); | ||
| 18 | + $table->timestamps(); | ||
| 19 | + }); | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * Reverse the migrations. | ||
| 24 | + * | ||
| 25 | + * @return void | ||
| 26 | + */ | ||
| 27 | + public function down() | ||
| 28 | + { | ||
| 29 | + Schema::drop('settings'); | ||
| 30 | + } | ||
| 31 | +} |
| ... | @@ -10,7 +10,7 @@ | ... | @@ -10,7 +10,7 @@ |
| 10 | color: #222; | 10 | color: #222; |
| 11 | width: 250px; | 11 | width: 250px; |
| 12 | max-width: 100%; | 12 | max-width: 100%; |
| 13 | - -webkit-appearance:none; | 13 | + //-webkit-appearance:none; |
| 14 | &.neg, &.invalid { | 14 | &.neg, &.invalid { |
| 15 | border: 1px solid $negative; | 15 | border: 1px solid $negative; |
| 16 | } | 16 | } |
| ... | @@ -25,9 +25,10 @@ | ... | @@ -25,9 +25,10 @@ |
| 25 | label { | 25 | label { |
| 26 | display: block; | 26 | display: block; |
| 27 | line-height: 1.4em; | 27 | line-height: 1.4em; |
| 28 | - font-size: 0.9em; | 28 | + font-size: 0.94em; |
| 29 | font-weight: 500; | 29 | font-weight: 500; |
| 30 | - color: #333; | 30 | + color: #666; |
| 31 | + padding-bottom: 2px; | ||
| 31 | } | 32 | } |
| 32 | 33 | ||
| 33 | label.radio, label.checkbox { | 34 | label.radio, label.checkbox { | ... | ... |
| ... | @@ -485,4 +485,19 @@ body.dragging, body.dragging * { | ... | @@ -485,4 +485,19 @@ body.dragging, body.dragging * { |
| 485 | background-color: $negative; | 485 | background-color: $negative; |
| 486 | color: #EEE; | 486 | color: #EEE; |
| 487 | } | 487 | } |
| 488 | +} | ||
| 489 | + | ||
| 490 | +.setting-nav { | ||
| 491 | + margin-top: $-l; | ||
| 492 | + border-top: 1px solid #DDD; | ||
| 493 | + border-bottom: 1px solid #DDD; | ||
| 494 | + a { | ||
| 495 | + padding: $-m; | ||
| 496 | + display: inline-block; | ||
| 497 | + //color: #666; | ||
| 498 | + &.selected { | ||
| 499 | + //color: $primary; | ||
| 500 | + background-color: #f8f8f8; | ||
| 501 | + } | ||
| 502 | + } | ||
| 488 | } | 503 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -54,7 +54,7 @@ | ... | @@ -54,7 +54,7 @@ |
| 54 | <header> | 54 | <header> |
| 55 | <div class="padded row clearfix"> | 55 | <div class="padded row clearfix"> |
| 56 | <div class="col-md-12 logo-container"> | 56 | <div class="col-md-12 logo-container"> |
| 57 | - <a href="/" class="logo">BookStack</a> | 57 | + <a href="/" class="logo">{{ Setting::get('app-name', 'BookStack') }}</a> |
| 58 | <div class="user-overview"> | 58 | <div class="user-overview"> |
| 59 | <img class="avatar" src="{{Auth::user()->getAvatar(50)}}" alt="{{ Auth::user()->name }}"> | 59 | <img class="avatar" src="{{Auth::user()->getAvatar(50)}}" alt="{{ Auth::user()->name }}"> |
| 60 | <span class="user-name"> | 60 | <span class="user-name"> | ... | ... |
resources/views/settings/index.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + @include('settings/navbar', ['selected' => 'settings']) | ||
| 6 | + | ||
| 7 | + <div class="page-content"> | ||
| 8 | + <h1>Settings</h1> | ||
| 9 | + | ||
| 10 | + <form action="/settings" method="POST"> | ||
| 11 | + {!! csrf_field() !!} | ||
| 12 | + <div class="form-group"> | ||
| 13 | + <label for="setting-app-name">Application Name</label> | ||
| 14 | + <input type="text" value="{{ Setting::get('app-name') }}" name="setting-app-name" id="setting-app-name"> | ||
| 15 | + </div> | ||
| 16 | + <div class="form-group"> | ||
| 17 | + <button type="submit" class="button pos">Update Settings</button> | ||
| 18 | + </div> | ||
| 19 | + </form> | ||
| 20 | + | ||
| 21 | + </div> | ||
| 22 | + | ||
| 23 | +@stop | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
resources/views/settings/navbar.blade.php
0 → 100644
| 1 | +<div class="row"> | ||
| 2 | + <div class="col-md-6 col-md-offset-3 setting-nav"> | ||
| 3 | + <a href="/settings" @if($selected == 'settings') class="selected" @endif><i class="zmdi zmdi-settings"></i>Settings</a> | ||
| 4 | + <a href="/users" @if($selected == 'users') class="selected" @endif><i class="zmdi zmdi-accounts"></i>Users</a> | ||
| 5 | + </div> | ||
| 6 | +</div> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
| 17 | 17 | ||
| 18 | <div class="row"> | 18 | <div class="row"> |
| 19 | <div class="col-md-6"> | 19 | <div class="col-md-6"> |
| 20 | - <h1>Edit User</h1> | 20 | + <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1> |
| 21 | <form action="/users/{{$user->id}}" method="post"> | 21 | <form action="/users/{{$user->id}}" method="post"> |
| 22 | {!! csrf_field() !!} | 22 | {!! csrf_field() !!} |
| 23 | <input type="hidden" name="_method" value="put"> | 23 | <input type="hidden" name="_method" value="put"> | ... | ... |
| ... | @@ -3,21 +3,15 @@ | ... | @@ -3,21 +3,15 @@ |
| 3 | 3 | ||
| 4 | @section('content') | 4 | @section('content') |
| 5 | 5 | ||
| 6 | - | 6 | + @include('settings/navbar', ['selected' => 'users']) |
| 7 | - <div class="row faded-small"> | ||
| 8 | - <div class="col-md-6"></div> | ||
| 9 | - <div class="col-md-6 faded"> | ||
| 10 | - <div class="action-buttons"> | ||
| 11 | - @if($currentUser->can('user-create')) | ||
| 12 | - <a href="/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>New User</a> | ||
| 13 | - @endif | ||
| 14 | - </div> | ||
| 15 | - </div> | ||
| 16 | - </div> | ||
| 17 | - | ||
| 18 | 7 | ||
| 19 | <div class="page-content"> | 8 | <div class="page-content"> |
| 20 | <h1>Users</h1> | 9 | <h1>Users</h1> |
| 10 | + @if($currentUser->can('user-create')) | ||
| 11 | + <p> | ||
| 12 | + <a href="/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add New User</a> | ||
| 13 | + </p> | ||
| 14 | + @endif | ||
| 21 | <table class="table"> | 15 | <table class="table"> |
| 22 | <tr> | 16 | <tr> |
| 23 | <th></th> | 17 | <th></th> | ... | ... |
-
Please register or sign in to post a comment