From 49cc935ceb790a5c140d33b8161fc0f576b21fb7 Mon Sep 17 00:00:00 2001 From: Xu Date: Thu, 14 Mar 2024 18:33:57 +0100 Subject: [PATCH] driving license: can be confirmed and edited by admins and config options --- config/config.default.php | 3 + config/routes.php | 1 + db/factories/User/LicenseFactory.php | 8 ++ ..._add_drive_confirmed_to_users_licenses.php | 31 +++++ ..._000001_add_user_drive_edit_permission.php | 46 +++++++ includes/controller/angeltypes_controller.php | 8 +- includes/controller/users_controller.php | 13 +- includes/view/AngelTypes_view.php | 122 +++++++++++------- includes/view/User_view.php | 9 +- resources/lang/de_DE/default.po | 5 +- resources/lang/en_US/default.po | 5 +- .../pages/settings/certificates-admin.twig | 41 ++++++ .../views/pages/settings/certificates.twig | 31 +++-- .../Admin/UserSettingsController.php | 80 +++++++++++- src/Controllers/SettingsController.php | 34 +++-- src/Models/User/License.php | 5 + .../Admin/UserSettingsControllerTest.php | 108 +++++++++++++++- .../Controllers/SettingsControllerTest.php | 53 +++++++- 18 files changed, 515 insertions(+), 88 deletions(-) create mode 100644 db/migrations/2024_03_13_000000_add_drive_confirmed_to_users_licenses.php create mode 100644 db/migrations/2024_03_13_000001_add_user_drive_edit_permission.php diff --git a/config/config.default.php b/config/config.default.php index bee87e45..e9b413c6 100644 --- a/config/config.default.php +++ b/config/config.default.php @@ -364,6 +364,9 @@ return [ 'voucher_start' => env('VOUCHER_START', null) ?: null, ], + // Enables Driving License + 'driving_license_enabled' => (bool) env('DRIVING_LICENSE_ENABLED', true), + # Instruction in accordance with § 43 Para. 1 of the German Infection Protection Act (IfSG) 'ifsg_enabled' => (bool) env('IFSG_ENABLED', false), diff --git a/config/routes.php b/config/routes.php index 0e8bcdbc..b1322eb3 100644 --- a/config/routes.php +++ b/config/routes.php @@ -57,6 +57,7 @@ $route->addGroup( function (RouteCollector $route): void { $route->get('/certificates', 'Admin\\UserSettingsController@certificate'); $route->post('/certificates/ifsg', 'Admin\\UserSettingsController@saveIfsgCertificate'); + $route->post('/certificates/driving', 'Admin\\UserSettingsController@saveDrivingLicense'); } ); diff --git a/db/factories/User/LicenseFactory.php b/db/factories/User/LicenseFactory.php index 19f0c7e8..ad4e4874 100644 --- a/db/factories/User/LicenseFactory.php +++ b/db/factories/User/LicenseFactory.php @@ -21,6 +21,13 @@ class LicenseFactory extends Factory $drive_12t = $drive_7_5t && $this->faker->boolean(.3); $drive_forklift = ($drive_car && $this->faker->boolean(.1)) || ($drive_12t && $this->faker->boolean(.7)); + $drive_confirmed = $this->faker->boolean(0.5) && ( + $drive_car + || $drive_3_5t + || $drive_7_5t + || $drive_12t + || $drive_forklift + ); $ifsg_certificate = $this->faker->boolean(0.1); $ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate; @@ -34,6 +41,7 @@ class LicenseFactory extends Factory 'drive_3_5t' => $drive_3_5t, 'drive_7_5t' => $drive_7_5t, 'drive_12t' => $drive_12t, + 'drive_confirmed' => $drive_confirmed, 'ifsg_certificate' => $ifsg_certificate, 'ifsg_certificate_light' => $ifsg_certificate_light, 'ifsg_confirmed' => $ifsg_confirmed, diff --git a/db/migrations/2024_03_13_000000_add_drive_confirmed_to_users_licenses.php b/db/migrations/2024_03_13_000000_add_drive_confirmed_to_users_licenses.php new file mode 100644 index 00000000..cf6858e9 --- /dev/null +++ b/db/migrations/2024_03_13_000000_add_drive_confirmed_to_users_licenses.php @@ -0,0 +1,31 @@ +schema->table('users_licenses', function (Blueprint $table): void { + $table->boolean('drive_confirmed')->default(false)->after('drive_12t'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_licenses', function (Blueprint $table): void { + $table->dropColumn('drive_confirmed'); + }); + } +} diff --git a/db/migrations/2024_03_13_000001_add_user_drive_edit_permission.php b/db/migrations/2024_03_13_000001_add_user_drive_edit_permission.php new file mode 100644 index 00000000..6fc4a9da --- /dev/null +++ b/db/migrations/2024_03_13_000001_add_user_drive_edit_permission.php @@ -0,0 +1,46 @@ +schema->getConnection(); + $db->table('privileges') + ->insert([ + 'name' => 'user.drive.edit', 'description' => 'Edit Driving License', + ]); + + $editDrive = $db->table('privileges') + ->where('name', 'user.drive.edit') + ->get(['id']) + ->first(); + + $shico = 60; + $team_coordinator = 65; + $db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $shico, 'privilege_id' => $editDrive->id], + ['group_id' => $team_coordinator, 'privilege_id' => $editDrive->id], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->where('name', 'user.drive.edit') + ->delete(); + } +} diff --git a/includes/controller/angeltypes_controller.php b/includes/controller/angeltypes_controller.php index 89652a25..37567508 100644 --- a/includes/controller/angeltypes_controller.php +++ b/includes/controller/angeltypes_controller.php @@ -140,8 +140,12 @@ function angeltype_edit_controller() engelsystem_log( 'Saved angeltype: ' . $angeltype->name . ($angeltype->restricted ? ', restricted' : '') . ($angeltype->shift_self_signup ? ', shift_self_signup' : '') - . ($angeltype->requires_driver_license ? ', requires driver license' : '') . ', ' - . ($angeltype->requires_ifsg_certificate ? ', requires ifsg certificate' : '') . ', ' + . (config('driving_license_enabled') + ? (($angeltype->requires_driver_license ? ', requires driver license' : '') . ', ') + : '') + . (config('ifsg_enabled') + ? (($angeltype->requires_ifsg_certificate ? ', requires ifsg certificate' : '') . ', ') + : '') . $angeltype->contact_name . ', ' . $angeltype->contact_dect . ', ' . $angeltype->contact_email . ', ' diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 6f7b900b..7e8259f7 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -254,6 +254,12 @@ function user_controller() ->where('user_angel_type.supporter', true) ->count(); + $is_drive_supporter = (bool) AngelType::whereRequiresDriverLicense(true) + ->leftJoin('user_angel_type', 'user_angel_type.angel_type_id', 'angel_types.id') + ->where('user_angel_type.user_id', $user->id) + ->where('user_angel_type.supporter', true) + ->count(); + return [ htmlspecialchars($user_source->displayName), User_view( @@ -268,7 +274,10 @@ function user_controller() auth()->can('admin_active'), auth()->can('admin_user_worklog'), $worklogs, - auth()->can('user.ifsg.edit') || $is_ifsg_supporter, + auth()->can('user.ifsg.edit') + || $is_ifsg_supporter + || auth()->can('user.drive.edit') + || $is_drive_supporter, ), ]; } @@ -463,7 +472,7 @@ function user_driver_license_required_hint() $user = auth()->user(); // User has already entered data, no hint needed. - if ($user->license->wantsToDrive()) { + if (!config('driving_license_enabled') || $user->license->wantsToDrive()) { return null; } diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php index 1bdc3880..1c8afc25 100644 --- a/includes/view/AngelTypes_view.php +++ b/includes/view/AngelTypes_view.php @@ -83,9 +83,39 @@ function AngelType_delete_view(AngelType $angeltype) */ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode) { + $requires_ifsg = ''; + $requires_driving_license = ''; + if (config('ifsg_enabled')) { + $requires_ifsg = $supporter_mode ? + form_info( + __('angeltype.ifsg.required'), + $angeltype->requires_ifsg_certificate + ? __('Yes') + : __('No') + ) : form_checkbox( + 'requires_ifsg_certificate', + __('angeltype.ifsg.required'), + $angeltype->requires_ifsg_certificate + ); + } + if (config('driving_license_enabled')) { + $requires_driving_license = $supporter_mode ? + form_info( + __('Requires driver license'), + $angeltype->requires_driver_license + ? __('Yes') + : __('No') + ) : form_checkbox( + 'requires_driver_license', + __('Requires driver license'), + $angeltype->requires_driver_license + ); + } + $link = button($angeltype->id ? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]) : url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( $link . ' ' . ( $angeltype->id ? @@ -120,30 +150,8 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode) __('angeltypes.shift.self_signup.info') . '">', $angeltype->shift_self_signup ), - $supporter_mode ? - form_info( - __('Requires driver license'), - $angeltype->requires_driver_license - ? __('Yes') - : __('No') - ) : - form_checkbox( - 'requires_driver_license', - __('Requires driver license'), - $angeltype->requires_driver_license - ), - $supporter_mode && config('ifsg_enabled') ? - form_info( - __('angeltype.ifsg.required'), - $angeltype->requires_ifsg_certificate - ? __('Yes') - : __('No') - ) : - form_checkbox( - 'requires_ifsg_certificate', - __('angeltype.ifsg.required'), - $angeltype->requires_ifsg_certificate - ), + $requires_driving_license, + $requires_ifsg, $supporter_mode ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), @@ -182,7 +190,7 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode) * @param UserAngelType|null $user_angeltype * @param bool $admin_angeltypes * @param bool $supporter - * @param License $user_driver_license + * @param License $user_license * @param User|null $user * @return string */ @@ -191,10 +199,10 @@ function AngelType_view_buttons( ?UserAngelType $user_angeltype, $admin_angeltypes, $supporter, - $user_driver_license, + $user_license, $user ) { - if ($angeltype->requires_driver_license) { + if (config('driving_license_enabled') && $angeltype->requires_driver_license) { $buttons[] = button( url('/settings/certificates'), icon('person-vcard') . __('My driving license') @@ -216,7 +224,7 @@ function AngelType_view_buttons( ($admin_angeltypes ? 'Join' : ''), ); } else { - if ($angeltype->requires_driver_license && !$user_driver_license->wantsToDrive()) { + if (config('driving_license_enabled') && $angeltype->requires_driver_license && !$user_license->wantsToDrive()) { error(__('This angeltype requires a driver license. Please enter your driver license information!')); } @@ -265,6 +273,13 @@ function AngelType_view_buttons( return buttons($buttons); } +function certificateIcon($confirmed, $certificate) +{ + return ($confirmed && $certificate) + ? icon('check2-all', 'text-success') + : icon_bool($certificate); +} + /** * Renders and sorts the members of an angeltype into supporters, members and unconfirmed members. * @@ -284,32 +299,36 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange if (config('enable_dect')) { $member['dect'] = htmlspecialchars((string) $member->contact->dect); } - if ($angeltype->requires_driver_license) { - $member['wants_to_drive'] = icon_bool($member->license->wantsToDrive()); + if (config('driving_license_enabled') && $angeltype->requires_driver_license) { + $drive_confirmed = $member->license->drive_confirmed; + $member['wants_to_drive'] = certificateIcon($drive_confirmed, $member->license->wantsToDrive()); $member['has_car'] = icon_bool($member->license->has_car); - $member['has_license_car'] = icon_bool($member->license->drive_car); - $member['has_license_3_5t_transporter'] = icon_bool($member->license->drive_3_5t); - $member['has_license_7_5t_truck'] = icon_bool($member->license->drive_7_5t); - $member['has_license_12t_truck'] = icon_bool($member->license->drive_12t); - $member['has_license_forklift'] = icon_bool($member->license->drive_forklift); + $member['has_license_car'] = certificateIcon($drive_confirmed, $member->license->drive_car); + $member['has_license_3_5t_transporter'] = certificateIcon($drive_confirmed, $member->license->drive_3_5t); + $member['has_license_7_5t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_7_5t); + $member['has_license_12t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_12t); + $member['has_license_forklift'] = certificateIcon($drive_confirmed, $member->license->drive_forklift); } - if ($angeltype->requires_ifsg_certificate && config('ifsg_enabled')) { - $ifsg_certificate = $member->license->ifsg_certificate; - $member['ifsg_certificate'] = ($member->license->ifsg_confirmed && $ifsg_certificate) - ? icon('check2-all', 'text-success') - : icon_bool($ifsg_certificate); + if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate) { + $ifsg_confirmed = $member->license->ifsg_confirmed; + $member['ifsg_certificate'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate); if (config('ifsg_light_enabled')) { - $ifsg_certificate_light = $member->license->ifsg_certificate_light; - $member['ifsg_certificate_light'] = ($member->license->ifsg_confirmed && $ifsg_certificate_light) - ? icon('check2-all', 'text-success') - : icon_bool($ifsg_certificate_light); + $member['ifsg_certificate_light'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate_light); } } $edit_certificates = ''; if ( - ($angeltype->requires_driver_license || $angeltype->requires_ifsg_certificate) - && ($admin_user_angeltypes || auth()->can('user.ifsg.edit')) + ( + config('driving_license_enabled') + && $angeltype->requires_driver_license + && ($admin_user_angeltypes || auth()->can('user.drive.edit')) + ) + || ( + config('ifsg_enabled') + && $angeltype->requires_ifsg_certificate + && ($admin_user_angeltypes || auth()->can('user.ifsg.edit')) + ) ) { $edit_certificates = button( @@ -425,7 +444,10 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a $headers['dect'] = __('general.dect'); } - if ($angeltype->requires_driver_license && ($supporter || $admin_angeltypes)) { + if ( + config('driving_license_enabled') && $angeltype->requires_driver_license + && ($supporter || $admin_angeltypes || auth()->can('user.drive.edit')) + ) { $headers = array_merge($headers, [ 'wants_to_drive' => __('Driver'), 'has_car' => __('Has car'), @@ -461,7 +483,7 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a * @param bool $admin_user_angeltypes * @param bool $admin_angeltypes * @param bool $supporter - * @param License $user_driver_license + * @param License $user_license * @param User $user * @param ShiftsFilterRenderer $shiftsFilterRenderer * @param ShiftCalendarRenderer $shiftCalendarRenderer @@ -475,7 +497,7 @@ function AngelType_view( $admin_user_angeltypes, $admin_angeltypes, $supporter, - $user_driver_license, + $user_license, $user, ShiftsFilterRenderer $shiftsFilterRenderer, ShiftCalendarRenderer $shiftCalendarRenderer, @@ -485,7 +507,7 @@ function AngelType_view( return page_with_title( $link . ' ' . sprintf(__('Team %s'), htmlspecialchars($angeltype->name)), [ - AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_driver_license, $user), + AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_license, $user), msg(), tabs([ __('Info') => AngelType_view_info( diff --git a/includes/view/User_view.php b/includes/view/User_view.php index 27fa6197..84e9f4b7 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -553,7 +553,7 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege) * @param bool $tshirt_admin * @param bool $admin_user_worklog_privilege * @param Worklog[]|Collection $user_worklogs - * @param bool $admin_ifsg + * @param bool $admin_certificates * * @return string */ @@ -569,7 +569,7 @@ function User_view( $tshirt_admin, $admin_user_worklog_privilege, $user_worklogs, - $admin_ifsg + $admin_certificates ) { $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; @@ -653,7 +653,10 @@ function User_view( icon('valentine') . __('Vouchers') ) : '', - $admin_ifsg ? button( + ( + $admin_certificates + && (config('ifsg_enabled') || config('driving_license_enabled')) + ) ? button( url('/users/' . $user_source->id . '/certificates'), icon('card-checklist') . __('settings.certificates') ) : '', diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 176e567e..bbfac6a1 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -1552,7 +1552,7 @@ msgid "news.comments.delete.title" msgstr "Kommentar \"%s\" löschen" msgid "notification.news.updated.introduction" -msgstr "Die News %1$s wurde aktualisiert" +msgstr "Die News \"%1$s\" wurde aktualisiert" msgid "notification.news.updated.text" msgstr "Du kannst sie dir unter %3$s anschauen." @@ -1754,6 +1754,9 @@ msgstr "Zertifikat bestätigt" msgid "settings.certificates.ifsg_confirmed.hint" msgstr "Deine Gesundheitsbelehrung wurde bestätigt, du kannst deine Angaben nicht mehr selber ändern." +msgid "settings.certificates.drive_confirmed.hint" +msgstr "Dein Führerschein wurde bestätigt, du kannst deine Angaben nicht mehr selber ändern." + msgid "settings.certificates.confirmation.info" msgstr "Du hast persönlich überprüft, dass die Zertifizierung / Bescheinigung den Anforderungen genügt." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 0b8371fc..9c1a8889 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -254,7 +254,7 @@ msgid "news.comments.delete.title" msgstr "Delete comment \"%s\"" msgid "notification.news.updated.introduction" -msgstr "The news %1$s was updated" +msgstr "The news \"%1$s\" was updated" msgid "notification.news.updated.text" msgstr "You can view it at %3$s" @@ -456,6 +456,9 @@ msgstr "Certificate confirmed" msgid "settings.certificates.ifsg_confirmed.hint" msgstr "Your health instruction has been confirmed, you can no longer change it by yourself." +msgid "settings.certificates.drive_confirmed.hint" +msgstr "Your driving license has been confirmed, you can no longer change it by yourself." + msgid "settings.certificates.confirmation.info" msgstr "You personally checked that the certificate / license meets the requirements." diff --git a/resources/views/pages/settings/certificates-admin.twig b/resources/views/pages/settings/certificates-admin.twig index f993f97b..a61c754c 100644 --- a/resources/views/pages/settings/certificates-admin.twig +++ b/resources/views/pages/settings/certificates-admin.twig @@ -38,6 +38,47 @@ {% endif %} + {% if config('driving_license_enabled') %} +
+ {{ csrf() }} +
+

{{ __('settings.certificates.driving_license') }}

+ + {{ f.checkbox('drive_car', __('settings.certificates.drive_car'), { + 'checked': certificates.drive_car, + }) }} + {{ f.checkbox('drive_3_5t', __('settings.certificates.drive_3_5t'), { + 'checked': certificates.drive_3_5t, + }) }} + {{ f.checkbox('drive_7_5t', __('settings.certificates.drive_7_5t'), { + 'checked': certificates.drive_7_5t, + }) }} + {{ f.checkbox('drive_12t', __('settings.certificates.drive_12t'), { + 'checked': certificates.drive_12t, + }) }} + {{ f.checkbox('drive_forklift', __('settings.certificates.drive_forklift'), { + 'checked': certificates.drive_forklift, + }) }} + +
+
+ {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} +
+ +
+ {{ f.checkbox( + 'drive_confirmed', + __('settings.certificates.confirmed') ~ f.info(__('settings.certificates.confirmation.info')), + {'raw_label': true, 'checked': certificates.drive_confirmed} + ) }} +
+
+
+
+ {% endif %} {% endblock %} diff --git a/resources/views/pages/settings/certificates.twig b/resources/views/pages/settings/certificates.twig index 3fba994d..534d8868 100644 --- a/resources/views/pages/settings/certificates.twig +++ b/resources/views/pages/settings/certificates.twig @@ -6,7 +6,7 @@ {% block row_content %}
- {% if config('ifsg_enabled') %} + {% if config('ifsg_enabled') and ifsg %}
{% endif %} - {% if driving_license %} - + {% if config('driving_license_enabled') and driving_license %} + {{ csrf() }}

{{ __('settings.certificates.driving_license') }}

{{ m.info(__('settings.certificates.info')) }} + {% if certificates.drive_confirmed %} +

{{ __('settings.certificates.drive_confirmed.hint') }}

+ {% endif %} {{ f.checkbox('drive_car', __('settings.certificates.drive_car'), { - 'checked': certificates.drive_car, + 'checked': certificates.drive_car, + 'disabled': certificates.drive_confirmed, }) }} + {{ f.checkbox('drive_3_5t', __('settings.certificates.drive_3_5t'), { - 'checked': certificates.drive_3_5t, + 'checked': certificates.drive_3_5t, + 'disabled': certificates.drive_confirmed, }) }} + {{ f.checkbox('drive_7_5t', __('settings.certificates.drive_7_5t'), { - 'checked': certificates.drive_7_5t, + 'checked': certificates.drive_7_5t, + 'disabled': certificates.drive_confirmed, }) }} + {{ f.checkbox('drive_12t', __('settings.certificates.drive_12t'), { - 'checked': certificates.drive_12t, + 'checked': certificates.drive_12t, + 'disabled': certificates.drive_confirmed, }) }} + {{ f.checkbox('drive_forklift', __('settings.certificates.drive_forklift'), { - 'checked': certificates.drive_forklift, + 'checked': certificates.drive_forklift, + 'disabled': certificates.drive_confirmed, }) }} {{ f.checkbox('has_car', __('settings.certificates.has_car'), { diff --git a/src/Controllers/Admin/UserSettingsController.php b/src/Controllers/Admin/UserSettingsController.php index 0b753f9e..a90ca3c0 100644 --- a/src/Controllers/Admin/UserSettingsController.php +++ b/src/Controllers/Admin/UserSettingsController.php @@ -32,11 +32,21 @@ class UserSettingsController extends BaseController public function certificate(Request $request): Response { - if (!config('ifsg_enabled')) { + if (!config('ifsg_enabled') && !config('driving_license_enabled')) { throw new HttpNotFound(); } - $this->checkPermission('user.ifsg.edit', $this->isIfsgSupporter()); + if ( + !( + $this->auth->can('user.ifsg.edit') + || $this->auth->can('user.drive.edit') + || $this->isDriverLicenseSupporter() + || $this->isIfsgSupporter() + ) + ) { + throw new HttpForbidden(); + } + $user = $this->getUser($request); return $this->view( @@ -87,6 +97,54 @@ class UserSettingsController extends BaseController return $this->redirect->to('/users/' . $user->id . '/certificates'); } + public function saveDrivingLicense(Request $request): Response + { + if (!config('driving_license_enabled')) { + throw new HttpNotFound(); + } + + $this->checkPermission('user.drive.edit', $this->isDriverLicenseSupporter()); + $user = $this->getUser($request); + + $data = $this->validate($request, [ + 'drive_car' => 'optional|checked', + 'drive_3_5t' => 'optional|checked', + 'drive_7_5t' => 'optional|checked', + 'drive_12t' => 'optional|checked', + 'drive_forklift' => 'optional|checked', + 'drive_confirmed' => 'optional|checked', + ]); + + $user->license->drive_car = (bool) $data['drive_car']; + $user->license->drive_3_5t = (bool) $data['drive_3_5t']; + $user->license->drive_7_5t = (bool) $data['drive_7_5t']; + $user->license->drive_12t = (bool) $data['drive_12t']; + $user->license->drive_forklift = (bool) $data['drive_forklift']; + $user->license->drive_confirmed = $data['drive_confirmed'] && ( + $user->license->drive_car + || $user->license->drive_3_5t + || $user->license->drive_7_5t + || $user->license->drive_12t + || $user->license->drive_forklift + ); + + $user->license->save(); + $this->addNotification('settings.certificates.success'); + + $this->log->info('Certificate "{certificate}" of user {user} ({id}) is {confirmation}.', [ + 'certificate' => ($user->license->drive_car ? 'car' : '') + . ($user->license->drive_3_5t ? ', 3.5t' : '') + . ($user->license->drive_7_5t ? ', 7.5t' : '') + . ($user->license->drive_12t ? ', 12t' : '') + . ($user->license->drive_forklift ? ', forklift' : ''), + 'user' => $user->name, + 'id' => $user->id, + 'confirmation' => $user->license->drive_confirmed ? 'confirmed' : 'unconfirmed', + ]); + + return $this->redirect->to('/users/' . $user->id . '/certificates'); + } + public function settingsMenu(User $user): array { $menu = [ @@ -95,11 +153,16 @@ class UserSettingsController extends BaseController ], ]; - if (config('ifsg_enabled')) { + if (config('ifsg_enabled') || config('driving_license_enabled')) { $menu[url('/users/' . $user->id . '/certificates')] = [ 'title' => 'settings.certificates', 'icon' => 'card-checklist', - 'permission' => $this->isIfsgSupporter() ? null : 'user.ifsg.edit', + 'permission' => ( + $this->auth->can('user.ifsg.edit') + || $this->isIfsgSupporter() + || $this->auth->can('user.drive.edit') + || $this->isDriverLicenseSupporter() + ) ? null : '_', ]; } @@ -139,4 +202,13 @@ class UserSettingsController extends BaseController ->where('user_angel_type.supporter', true) ->count(); } + + public function isDriverLicenseSupporter(): bool + { + return (bool) AngelType::whereRequiresDriverLicense(true) + ->leftJoin('user_angel_type', 'user_angel_type.angel_type_id', 'angel_types.id') + ->where('user_angel_type.user_id', $this->auth->user()?->id) + ->where('user_angel_type.supporter', true) + ->count(); + } } diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index e0c9697b..037568e3 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -242,7 +242,10 @@ class SettingsController extends BaseController public function certificate(): Response { - if (!config('ifsg_enabled') && !$this->checkDrivingLicense()) { + if ( + !(config('ifsg_enabled') && $this->checkIfsgCertificate()) + && !(config('driving_license_enabled') && $this->checkDrivingLicense()) + ) { throw new HttpNotFound(); } @@ -252,6 +255,7 @@ class SettingsController extends BaseController [ 'settings_menu' => $this->settingsMenu(), 'driving_license' => $this->checkDrivingLicense(), + 'ifsg' => $this->checkIfsgCertificate(), 'certificates' => $user->license, ] ); @@ -260,7 +264,7 @@ class SettingsController extends BaseController public function saveIfsgCertificate(Request $request): Response { $user = $this->auth->user(); - if (!config('ifsg_enabled') || $user->license->ifsg_confirmed) { + if (!config('ifsg_enabled') || $user->license->ifsg_confirmed || !$this->checkIfsgCertificate()) { throw new HttpNotFound(); } @@ -282,7 +286,7 @@ class SettingsController extends BaseController public function saveDrivingLicense(Request $request): Response { - if (!$this->checkDrivingLicense()) { + if (!config('driving_license_enabled') || !$this->checkDrivingLicense()) { throw new HttpNotFound(); } @@ -297,11 +301,13 @@ class SettingsController extends BaseController ]); $user->license->has_car = (bool) $data['has_car']; - $user->license->drive_car = (bool) $data['drive_car']; - $user->license->drive_3_5t = (bool) $data['drive_3_5t']; - $user->license->drive_7_5t = (bool) $data['drive_7_5t']; - $user->license->drive_12t = (bool) $data['drive_12t']; - $user->license->drive_forklift = (bool) $data['drive_forklift']; + if (!$user->license->drive_confirmed) { + $user->license->drive_car = (bool) $data['drive_car']; + $user->license->drive_3_5t = (bool) $data['drive_3_5t']; + $user->license->drive_7_5t = (bool) $data['drive_7_5t']; + $user->license->drive_12t = (bool) $data['drive_12t']; + $user->license->drive_forklift = (bool) $data['drive_forklift']; + } $user->license->save(); $this->addNotification('settings.certificates.success'); @@ -394,7 +400,10 @@ class SettingsController extends BaseController $menu[url('/settings/theme')] = 'settings.theme'; } - if (config('ifsg_enabled') || $this->checkDrivingLicense()) { + if ( + (config('ifsg_enabled') && $this->checkIfsgCertificate()) + || (config('driving_license_enabled') && $this->checkDrivingLicense()) + ) { $menu[url('/settings/certificates')] = ['title' => 'settings.certificates', 'icon' => 'card-checklist']; } @@ -429,6 +438,13 @@ class SettingsController extends BaseController })->isNotEmpty(); } + protected function checkIfsgCertificate(): bool + { + return $this->auth->user()->userAngelTypes->filter(function (AngelType $angelType) { + return $angelType->requires_ifsg_certificate; + })->isNotEmpty(); + } + private function isRequired(string $key): string { $requiredFields = $this->config->get('required_user_fields'); diff --git a/src/Models/User/License.php b/src/Models/User/License.php index 565ef292..17b5bac0 100644 --- a/src/Models/User/License.php +++ b/src/Models/User/License.php @@ -14,6 +14,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; * @property bool $drive_3_5t * @property bool $drive_7_5t * @property bool $drive_12t + * @property bool $drive_confirmed * @property bool $ifsg_certificate_light * @property bool $ifsg_certificate * @property bool $ifsg_confirmed @@ -24,6 +25,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; * @method static QueryBuilder|License[] whereDrive35T($value) * @method static QueryBuilder|License[] whereDrive75T($value) * @method static QueryBuilder|License[] whereDrive12T($value) + * @method static QueryBuilder|License[] whereDriveConfirmed($value) * @method static QueryBuilder|License[] whereIfsgCertificateLight($value) * @method static QueryBuilder|License[] whereIfsgCertificate($value) * @method static QueryBuilder|License[] whereIfsgConfirmed($value) @@ -43,6 +45,7 @@ class License extends HasUserModel 'drive_3_5t' => false, 'drive_7_5t' => false, 'drive_12t' => false, + 'drive_confirmed' => false, 'ifsg_certificate_light' => false, 'ifsg_certificate' => false, 'ifsg_confirmed' => false, @@ -61,6 +64,7 @@ class License extends HasUserModel 'drive_3_5t', 'drive_7_5t', 'drive_12t', + 'drive_confirmed', 'ifsg_certificate_light', 'ifsg_certificate', 'ifsg_confirmed', @@ -74,6 +78,7 @@ class License extends HasUserModel 'drive_3_5t' => 'boolean', 'drive_7_5t' => 'boolean', 'drive_12t' => 'boolean', + 'drive_confirmed' => 'boolean', 'ifsg_certificate_light' => 'boolean', 'ifsg_certificate' => 'boolean', 'ifsg_confirmed' => 'boolean', diff --git a/tests/Unit/Controllers/Admin/UserSettingsControllerTest.php b/tests/Unit/Controllers/Admin/UserSettingsControllerTest.php index 96b9d42a..4d25a99b 100644 --- a/tests/Unit/Controllers/Admin/UserSettingsControllerTest.php +++ b/tests/Unit/Controllers/Admin/UserSettingsControllerTest.php @@ -42,11 +42,13 @@ class UserSettingsControllerTest extends ControllerTest } /** - * @covers \Engelsystem\Controllers\Admin\UserSettingsController::checkPermission + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::certificate + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::isIfsgSupporter + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::isDriverLicenseSupporter */ public function testCertificateNotAllowed(): void { - config(['ifsg_enabled' => true]); + config(['ifsg_enabled' => true, 'driving_license_enabled' => true]); $this->expectException(HttpForbidden::class); $this->controller->certificate($this->request); @@ -95,6 +97,22 @@ class UserSettingsControllerTest extends ControllerTest $this->controller->certificate($this->request); } + /** + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::certificate + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::checkPermission + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::isDriverLicenseSupporter + */ + public function testDriverLicenseByAngelTypeSupporter(): void + { + config(['driving_license_enabled' => true]); + $this->setExpects($this->response, 'withView', null, $this->response); + + $angelType = AngelType::factory()->create(['requires_driver_license' => true]); + $this->user->userAngelTypes()->attach($angelType, ['supporter' => true]); + + $this->controller->certificate($this->request); + } + /** * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveIfsgCertificate */ @@ -108,6 +126,7 @@ class UserSettingsControllerTest extends ControllerTest /** * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveIfsgCertificate + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::checkPermission */ public function testSaveIfsgCertificateNotAllowed(): void { @@ -194,6 +213,91 @@ class UserSettingsControllerTest extends ControllerTest $this->assertFalse($this->userChanged->license->ifsg_confirmed); } + /** + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveDrivingLicense + */ + public function testSaveDrivingLicenseDisabled(): void + { + config(['driving_license_enabled' => false]); + + $this->expectException(HttpNotFound::class); + $this->controller->saveDrivingLicense($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveDrivingLicense + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::checkPermission + */ + public function testSaveDrivingLicenseNotAllowed(): void + { + config(['driving_license_enabled' => true]); + + $this->expectException(HttpForbidden::class); + $this->controller->saveDrivingLicense($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveDrivingLicense + */ + public function testSaveDrivingLicenseConfirmed(): void + { + config(['driving_license_enabled' => true]); + $this->setExpects($this->auth, 'can', ['user.drive.edit'], true, $this->atLeastOnce()); + + $body = [ + 'drive_car' => true, + 'drive_3_5t' => true, + 'drive_confirmed' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/users/' . $this->userChanged->id . '/certificates') + ->willReturn($this->response); + + $this->controller->saveDrivingLicense($this->request); + $this->assertTrue($this->log->hasInfoThatContains('Certificate')); + + $this->assertFalse($this->userChanged->license->drive_forklift); + $this->assertFalse($this->userChanged->license->drive_12t); + $this->assertFalse($this->userChanged->license->drive_7_5t); + $this->assertFalse($this->userChanged->license->has_car); + $this->assertTrue($this->userChanged->license->drive_car); + $this->assertTrue($this->userChanged->license->drive_3_5t); + $this->assertTrue($this->userChanged->license->drive_confirmed); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserSettingsController::saveDrivingLicense + */ + public function testSaveDrivingLicense(): void + { + config(['driving_license_enabled' => true]); + $this->setExpects($this->auth, 'can', ['user.drive.edit'], true, $this->atLeastOnce()); + + $body = [ + 'drive_forklift' => true, + 'drive_12t' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/users/' . $this->userChanged->id . '/certificates') + ->willReturn($this->response); + + $this->controller->saveDrivingLicense($this->request); + + $this->assertFalse($this->userChanged->license->drive_3_5t); + $this->assertFalse($this->userChanged->license->drive_7_5t); + $this->assertFalse($this->userChanged->license->drive_car); + $this->assertFalse($this->userChanged->license->has_car); + $this->assertTrue($this->userChanged->license->drive_forklift); + $this->assertTrue($this->userChanged->license->drive_12t); + $this->assertFalse($this->userChanged->license->drive_confirmed); + } + /** * @covers \Engelsystem\Controllers\Admin\UserSettingsController::settingsMenu */ diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index 8aabac29..d8b47ca5 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -689,6 +689,9 @@ class SettingsControllerTest extends ControllerTest config(['ifsg_enabled' => true, 'ifsg_light_enabled' => true]); $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); + $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) { @@ -707,7 +710,6 @@ class SettingsControllerTest extends ControllerTest public function testCertificateIfsgNotConfigured(): void { config(['ifsg_enabled' => false]); - $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); $this->expectException(HttpNotFound::class); $this->controller->certificate(); @@ -718,6 +720,7 @@ class SettingsControllerTest extends ControllerTest */ public function testCertificateDrivingLicense(): void { + config(['driving_license_enabled' => true]); $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); $angelType = AngelType::factory()->create(['requires_driver_license' => true]); @@ -752,7 +755,10 @@ class SettingsControllerTest extends ControllerTest public function testSaveIfsgCertificateLight(): void { config(['ifsg_enabled' => true, 'ifsg_light_enabled' => true]); - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); $body = [ 'ifsg_certificate_light' => true, @@ -776,7 +782,11 @@ class SettingsControllerTest extends ControllerTest public function testSaveIfsgCertificateLightWhileDisabled(): void { config(['ifsg_enabled' => true, 'ifsg_light_enabled' => false]); - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); + $this->user->license->ifsg_certificate_light = false; $this->user->license->save(); @@ -798,11 +808,15 @@ class SettingsControllerTest extends ControllerTest /** * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + * @covers \Engelsystem\Controllers\SettingsController::checkIfsgCertificate */ public function testSaveIfsgCertificate(): void { config(['ifsg_enabled' => true]); - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); $body = [ 'ifsg_certificate' => true, @@ -826,7 +840,10 @@ class SettingsControllerTest extends ControllerTest public function testSaveIfsgCertificateBoth(): void { config(['ifsg_enabled' => true]); - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); $body = [ 'ifsg_certificate_light' => true, @@ -851,6 +868,7 @@ class SettingsControllerTest extends ControllerTest */ public function testSaveDrivingLicense(): void { + config(['driving_license_enabled' => true]); $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); $angelType = AngelType::factory()->create(['requires_driver_license' => true]); @@ -893,6 +911,7 @@ class SettingsControllerTest extends ControllerTest */ public function testApi(): void { + config(['ifsg_enabled' => true]); $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); /** @var Response|MockObject $response */ @@ -993,11 +1012,33 @@ class SettingsControllerTest extends ControllerTest /** * @covers \Engelsystem\Controllers\SettingsController::settingsMenu - * @covers \Engelsystem\Controllers\SettingsController::checkOauthHidden */ public function testSettingsMenuWithIfsg(): void { config(['ifsg_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_ifsg_certificate' => true]); + $this->user->userAngelTypes()->attach($angelType); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/certificates', $menu); + $this->assertEquals( + ['title' => 'settings.certificates', 'icon' => 'card-checklist'], + $menu['http://localhost/settings/certificates'] + ); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuWithDrivingLicense(): void + { + config(['driving_license_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_driver_license' => true]); + $this->user->userAngelTypes()->attach($angelType); $menu = $this->controller->settingsMenu(); $this->assertArrayHasKey('http://localhost/settings/certificates', $menu);