Dan Brown

Added user setting system and added user-lang option

Supports #115
1 -<?php 1 +<?php namespace BookStack\Http\Controllers;
2 2
3 -namespace BookStack\Http\Controllers;
4 -
5 -use BookStack\Activity;
6 use Exception; 3 use Exception;
7 use Illuminate\Http\Request; 4 use Illuminate\Http\Request;
8 -
9 use Illuminate\Http\Response; 5 use Illuminate\Http\Response;
10 -use BookStack\Http\Requests;
11 use BookStack\Repos\UserRepo; 6 use BookStack\Repos\UserRepo;
12 use BookStack\Services\SocialAuthService; 7 use BookStack\Services\SocialAuthService;
13 use BookStack\User; 8 use BookStack\User;
...@@ -152,7 +147,8 @@ class UserController extends Controller ...@@ -152,7 +147,8 @@ class UserController extends Controller
152 'name' => 'min:2', 147 'name' => 'min:2',
153 'email' => 'min:2|email|unique:users,email,' . $id, 148 'email' => 'min:2|email|unique:users,email,' . $id,
154 'password' => 'min:5|required_with:password_confirm', 149 'password' => 'min:5|required_with:password_confirm',
155 - 'password-confirm' => 'same:password|required_with:password' 150 + 'password-confirm' => 'same:password|required_with:password',
151 + 'setting' => 'array'
156 ]); 152 ]);
157 153
158 $user = $this->user->findOrFail($id); 154 $user = $this->user->findOrFail($id);
...@@ -175,6 +171,13 @@ class UserController extends Controller ...@@ -175,6 +171,13 @@ class UserController extends Controller
175 $user->external_auth_id = $request->get('external_auth_id'); 171 $user->external_auth_id = $request->get('external_auth_id');
176 } 172 }
177 173
174 + // Save an user-specific settings
175 + if ($request->has('setting')) {
176 + foreach ($request->get('setting') as $key => $value) {
177 + setting()->putUser($user, $key, $value);
178 + }
179 + }
180 +
178 $user->save(); 181 $user->save();
179 session()->flash('success', trans('settings.users_edit_success')); 182 session()->flash('success', trans('settings.users_edit_success'));
180 183
......
1 -<?php 1 +<?php namespace BookStack\Http;
2 -
3 -namespace BookStack\Http;
4 2
5 use Illuminate\Foundation\Http\Kernel as HttpKernel; 3 use Illuminate\Foundation\Http\Kernel as HttpKernel;
6 4
...@@ -30,6 +28,7 @@ class Kernel extends HttpKernel ...@@ -30,6 +28,7 @@ class Kernel extends HttpKernel
30 \Illuminate\View\Middleware\ShareErrorsFromSession::class, 28 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
31 \BookStack\Http\Middleware\VerifyCsrfToken::class, 29 \BookStack\Http\Middleware\VerifyCsrfToken::class,
32 \Illuminate\Routing\Middleware\SubstituteBindings::class, 30 \Illuminate\Routing\Middleware\SubstituteBindings::class,
31 + \BookStack\Http\Middleware\Localization::class
33 ], 32 ],
34 'api' => [ 33 'api' => [
35 'throttle:60,1', 34 'throttle:60,1',
......
1 +<?php
2 +
3 +namespace BookStack\Http\Middleware;
4 +
5 +use Carbon\Carbon;
6 +use Closure;
7 +
8 +class Localization
9 +{
10 + /**
11 + * Handle an incoming request.
12 + *
13 + * @param \Illuminate\Http\Request $request
14 + * @param \Closure $next
15 + * @return mixed
16 + */
17 + public function handle($request, Closure $next)
18 + {
19 + $defaultLang = config('app.locale');
20 + $locale = setting()->getUser(user(), 'language', $defaultLang);
21 + app()->setLocale($locale);
22 + Carbon::setLocale($locale);
23 + return $next($request);
24 + }
25 +}
1 -<?php 1 +<?php namespace BookStack\Http\Middleware;
2 -
3 -namespace BookStack\Http\Middleware;
4 2
5 use Closure; 3 use Closure;
6 use Illuminate\Contracts\Auth\Guard; 4 use Illuminate\Contracts\Auth\Guard;
......
1 <?php namespace BookStack\Providers; 1 <?php namespace BookStack\Providers;
2 2
3 -use Carbon\Carbon;
4 use Illuminate\Support\ServiceProvider; 3 use Illuminate\Support\ServiceProvider;
5 use Validator; 4 use Validator;
6 5
...@@ -18,8 +17,6 @@ class AppServiceProvider extends ServiceProvider ...@@ -18,8 +17,6 @@ class AppServiceProvider extends ServiceProvider
18 $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp']; 17 $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
19 return in_array($value->getMimeType(), $imageMimes); 18 return in_array($value->getMimeType(), $imageMimes);
20 }); 19 });
21 -
22 - Carbon::setLocale(config('app.locale'));
23 } 20 }
24 21
25 /** 22 /**
......
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 use BookStack\Setting; 3 use BookStack\Setting;
4 +use BookStack\User;
4 use Illuminate\Contracts\Cache\Repository as Cache; 5 use Illuminate\Contracts\Cache\Repository as Cache;
5 6
6 /** 7 /**
...@@ -44,6 +45,18 @@ class SettingService ...@@ -44,6 +45,18 @@ class SettingService
44 } 45 }
45 46
46 /** 47 /**
48 + * Get a user-specific setting from the database or cache.
49 + * @param User $user
50 + * @param $key
51 + * @param bool $default
52 + * @return bool|string
53 + */
54 + public function getUser($user, $key, $default = false)
55 + {
56 + return $this->get($this->userKey($user->id, $key), $default);
57 + }
58 +
59 + /**
47 * Gets a setting value from the cache or database. 60 * Gets a setting value from the cache or database.
48 * Looks at the system defaults if not cached or in database. 61 * Looks at the system defaults if not cached or in database.
49 * @param $key 62 * @param $key
...@@ -112,6 +125,16 @@ class SettingService ...@@ -112,6 +125,16 @@ class SettingService
112 } 125 }
113 126
114 /** 127 /**
128 + * Check if a user setting is in the database.
129 + * @param $key
130 + * @return bool
131 + */
132 + public function hasUser($key)
133 + {
134 + return $this->has($this->userKey($key));
135 + }
136 +
137 + /**
115 * Add a setting to the database. 138 * Add a setting to the database.
116 * @param $key 139 * @param $key
117 * @param $value 140 * @param $value
...@@ -129,6 +152,28 @@ class SettingService ...@@ -129,6 +152,28 @@ class SettingService
129 } 152 }
130 153
131 /** 154 /**
155 + * Put a user-specific setting into the database.
156 + * @param User $user
157 + * @param $key
158 + * @param $value
159 + * @return bool
160 + */
161 + public function putUser($user, $key, $value)
162 + {
163 + return $this->put($this->userKey($user->id, $key), $value);
164 + }
165 +
166 + /**
167 + * Convert a setting key into a user-specific key.
168 + * @param $key
169 + * @return string
170 + */
171 + protected function userKey($userId, $key = '')
172 + {
173 + return 'user:' . $userId . ':' . $key;
174 + }
175 +
176 + /**
132 * Removes a setting from the database. 177 * Removes a setting from the database.
133 * @param $key 178 * @param $key
134 * @return bool 179 * @return bool
...@@ -144,6 +189,16 @@ class SettingService ...@@ -144,6 +189,16 @@ class SettingService
144 } 189 }
145 190
146 /** 191 /**
192 + * Delete settings for a given user id.
193 + * @param $userId
194 + * @return mixed
195 + */
196 + public function deleteUserSettings($userId)
197 + {
198 + return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
199 + }
200 +
201 + /**
147 * Gets a setting model from the database for the given key. 202 * Gets a setting model from the database for the given key.
148 * @param $key 203 * @param $key
149 * @return mixed 204 * @return mixed
......
...@@ -60,11 +60,12 @@ function userCan($permission, Ownable $ownable = null) ...@@ -60,11 +60,12 @@ function userCan($permission, Ownable $ownable = null)
60 * Helper to access system settings. 60 * Helper to access system settings.
61 * @param $key 61 * @param $key
62 * @param bool $default 62 * @param bool $default
63 - * @return mixed 63 + * @return bool|string|\BookStack\Services\SettingService
64 */ 64 */
65 -function setting($key, $default = false) 65 +function setting($key = null, $default = false)
66 { 66 {
67 $settingService = app(\BookStack\Services\SettingService::class); 67 $settingService = app(\BookStack\Services\SettingService::class);
68 + if (is_null($key)) return $settingService;
68 return $settingService->get($key, $default); 69 return $settingService->get($key, $default);
69 } 70 }
70 71
......
...@@ -40,13 +40,19 @@ php artisan db:seed --class=DummyContentSeeder --database=mysql_testing ...@@ -40,13 +40,19 @@ php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
40 40
41 Once done you can run `phpunit` in the application root directory to run all tests. 41 Once done you can run `phpunit` in the application root directory to run all tests.
42 42
43 -## Website and Docs 43 +## Translations
44 44
45 -The website and project docs are currently stored in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. The docs are stored as markdown files in the `resources/docs` folder 45 +As part of BookStack v0.14 support for translations has been built in. All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
46 +
47 + Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
48 +
49 +## Website, Docs & Blog
50 +
51 +The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
46 52
47 ## License 53 ## License
48 54
49 -BookStack is provided under the MIT License. 55 +The BookStack source is provided under the MIT License.
50 56
51 ## Attribution 57 ## Attribution
52 58
......
...@@ -101,12 +101,23 @@ return [ ...@@ -101,12 +101,23 @@ return [
101 'users_edit_success' => 'User successfully updated', 101 'users_edit_success' => 'User successfully updated',
102 'users_avatar' => 'User Avatar', 102 'users_avatar' => 'User Avatar',
103 'users_avatar_desc' => 'This image should be approx 256px square.', 103 'users_avatar_desc' => 'This image should be approx 256px square.',
104 + 'users_preferred_language' => 'Preferred Language',
104 'users_social_accounts' => 'Social Accounts', 105 'users_social_accounts' => 'Social Accounts',
105 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', 106 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
106 'users_social_connect' => 'Connect Account', 107 'users_social_connect' => 'Connect Account',
107 'users_social_disconnect' => 'Disconnect Account', 108 'users_social_disconnect' => 'Disconnect Account',
108 'users_social_connected' => ':socialAccount account was successfully attached to your profile.', 109 'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
109 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', 110 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
111 +
112 + // Since these labels are already localized this array does not need to be
113 + // translated in the language-specific files.
114 + // DELETE BELOW IF COPIED FROM EN
115 + ///////////////////////////////////
116 + 'language_select' => [
117 + 'en' => 'English',
118 + 'de' => 'Deutsch'
119 + ]
120 + ///////////////////////////////////
110 ]; 121 ];
111 122
112 123
......
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
5 5
6 @include('settings/navbar', ['selected' => 'users']) 6 @include('settings/navbar', ['selected' => 'users'])
7 7
8 -
9 -
10 <div class="container small"> 8 <div class="container small">
11 <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post"> 9 <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post">
12 <div class="row"> 10 <div class="row">
...@@ -42,7 +40,14 @@ ...@@ -42,7 +40,14 @@
42 'name' => 'image_id', 40 'name' => 'image_id',
43 'imageClass' => 'avatar large' 41 'imageClass' => 'avatar large'
44 ]) 42 ])
45 - 43 + </div>
44 + <div class="form-group">
45 + <label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
46 + <select name="setting[language]" id="user-language">
47 + @foreach(trans('settings.language_select') as $lang => $label)
48 + <option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
49 + @endforeach
50 + </select>
46 </div> 51 </div>
47 </div> 52 </div>
48 </div> 53 </div>
......
...@@ -583,7 +583,6 @@ class RolesTest extends TestCase ...@@ -583,7 +583,6 @@ class RolesTest extends TestCase
583 public function test_image_delete_own_permission() 583 public function test_image_delete_own_permission()
584 { 584 {
585 $this->giveUserPermissions($this->user, ['image-update-all']); 585 $this->giveUserPermissions($this->user, ['image-update-all']);
586 -// $admin = $this->getAdmin();
587 $page = \BookStack\Page::first(); 586 $page = \BookStack\Page::first();
588 $image = factory(\BookStack\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]); 587 $image = factory(\BookStack\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
589 588
......