diff --git a/assets/sass/forms.scss b/assets/sass/forms.scss index c367bca3..f765e88a 100644 --- a/assets/sass/forms.scss +++ b/assets/sass/forms.scss @@ -39,4 +39,22 @@ fieldset:empty { display: none; +} + +/* e.g. the work contract settings is user profile render rows with mb-3 and the last row doesn't need that */ +fieldset > .mb-3.row:last-child { + margin-bottom: 0!important; +} + +.form-fieldset-light { + border-radius: 0; + margin-bottom: 0; + border-top: none; + border-right: none; + border-left: none; + background-color: unset; +} + +.form-fieldset-light:last-child { + border:none; } \ No newline at end of file diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index 8cb6f741..cda11570 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -95,7 +95,7 @@ kimai: TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet','weekly_own_timesheet'] TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet'] PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile'] - PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile'] + PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile','supervisor_other_profile'] TAGS: ['view_tag','manage_tag','create_tag','delete_tag'] USER: ['view_user','create_user','delete_user','role_permissions'] RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet'] @@ -119,7 +119,7 @@ kimai: ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting'] ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member'] ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile'] - ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile'] + ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile'] # -------------------------------------------------------------------------------- diff --git a/migrations/Version20230819090536.php b/migrations/Version20230819090536.php new file mode 100644 index 00000000..ef8411c6 --- /dev/null +++ b/migrations/Version20230819090536.php @@ -0,0 +1,41 @@ +getTable('kimai2_users'); + $table->addColumn('supervisor_id', 'integer', ['length' => 11, 'notnull' => false, 'default' => null]); + $table->addForeignKeyConstraint('kimai2_users', ['supervisor_id'], ['id'], ['onDelete' => 'SET NULL'], 'FK_B9AC5BCE19E9AC5F'); + $table->addIndex(['supervisor_id'], 'IDX_B9AC5BCE19E9AC5F'); + } + + public function down(Schema $schema): void + { + $table = $schema->getTable('kimai2_users'); + + $table->removeForeignKey('FK_B9AC5BCE19E9AC5F'); + $table->dropIndex('IDX_B9AC5BCE19E9AC5F'); + $table->dropColumn('supervisor_id'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/public/build/app.eb575c90.css b/public/build/app.405a508c.css similarity index 67% rename from public/build/app.eb575c90.css rename to public/build/app.405a508c.css index 7b292b80..87d5c815 100644 --- a/public/build/app.eb575c90.css +++ b/public/build/app.405a508c.css @@ -1 +1 @@ -.inline-search{max-width:200px}.inline-search #searchTerm{border-right:0}@media (min-width:360px){.inline-search{max-width:235px}}@media (min-width:768px){.inline-search{max-width:325px}.search-dropdown{width:500px}}h1.navbar-brand a span{display:inline-block;padding-top:.5rem}h1.navbar-brand a:hover{text-decoration:none}#export-buttons .dropdown-menu,.inline-search .dropdown-menu,.page-actions .dropdown-menu{z-index:1021}.navbar{--tblr-navbar-brand-font-size:1rem}.error-page{margin-bottom:50px}@media print{.control-sidebar,.sf-toolbar{display:none!important}.col-print-12{width:100%}.content-wrapper{margin:0!important}section.content{margin:0;padding:0}span i.dot{display:none;margin-right:0!important}}.page-center{justify-content:center}.hidden-no-space{margin:0;padding:0}.pre-scrollable{max-height:340px;overflow-y:scroll}td.timesheet-description ul{padding-left:15px}td.timesheet-description p{margin:0}td.badges{line-height:22px}.open-edit{cursor:pointer}td.w-min,th.w-min{white-space:nowrap;width:1%}table.dataTable thead>tr>th.hw-min{white-space:normal;width:1%}.w-avatar{width:40px}.w-avatar img.avatar{max-width:40px}.list-group-item.danger{border-left:.2rem solid #d63939!important;padding-left:1.3rem}.list-group-item.success{border-left:.2rem solid #2fb344!important;padding-left:1.3rem}.box-body-scrollable{max-height:340px;overflow:auto}#activity_details_box th,#customer_details_box th,#project_details_box th{width:30%}.overlay{background:hsla(0,0%,100%,.7);height:100%;left:0;position:fixed;top:0;width:100%;z-index:1021}.overlay>.fas{color:#000;font-size:30px;left:50vw;margin-left:-15px;margin-top:-15px;position:absolute;top:50vh}table.dataTable{clear:both;margin-bottom:0!important;max-width:none!important}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer;position:relative}table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{font-weight:700}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after{font-family:Font Awesome\ 5 Free;font-size:12px;opacity:.5;padding-left:5px}table.dataTable thead .sorting:after{content:"\f0dc";opacity:.2}table.dataTable thead .sorting_asc:after{content:"\f077"}table.dataTable thead .sorting_desc:after{content:"\f078"}table.dataTable thead>tr>th{vertical-align:top;white-space:nowrap}table.dataTable th:active{outline:none}table.table-bordered thead td,table.table-bordered thead th{border-left-width:0;border-top-width:0}table.table-bordered tbody td,table.table-bordered tbody th,table.table-bordered tfoot td,table.table-bordered tfoot th{border-bottom-width:0;border-left-width:0}table.table-bordered td:last-child,table.table-bordered th:last-child{border-right-width:0}@media (min-width:1200px){table.dataTable.table>tbody>tr>td{vertical-align:middle}}table.dataTable .actions{width:40px}table.dataTable tr.summary td{background-color:var(--tblr-bg-surface-secondary);border:0;font-weight:700}table.dataTable tr th.multiCheckbox{width:15px}table.dataTable tr.overlapping{border-top:2px solid rgba(214,57,57,.1)}table.dataTable tr.exported{opacity:.7}table.dataTable td .btn-group{display:flex}table.dataTable td .label{font-size:85%;line-height:24px;padding:.3em .6em}table.dataTable td span.label-invisible{opacity:.6}table.dataTable td.weekend,table.dataTable th.weekend{background-color:var(--tblr-bg-surface-secondary)}table.dataTable th.today{background-color:var(--tblr-highlight-bg)}table.dataTable td.total,table.dataTable th.total{font-weight:700}.form-dataTable table.dataTable .form-group{margin-bottom:0}table.table-hover tr.exported:hover{opacity:1}.calendar-entry ul{list-style-type:none;padding:0}.calendar-entry div{white-space:pre-line}.draggable{cursor:grab}#timesheet_calendar{--bs-gray-400:var(--tblr-border-color);--bs-gray-200:var(--tblr-gray-200)}.fc-day-sat,.fc-day-sun{background-color:var(--tblr-bg-surface-secondary)}@keyframes ticktac-blink{0%{opacity:1}5%{opacity:.95}10%{opacity:.9}15%{opacity:.85}20%{opacity:.8}25%{opacity:.75}30%{opacity:.7}35%{opacity:.65}40%{opacity:.6}45%{opacity:.65}50%{opacity:.7}55%{opacity:.75}60%{opacity:.8}65%{opacity:.85}70%{opacity:.9}75%{opacity:.95}to{opacity:1}}.ticktac-start i,.ticktac-stop i{animation:ticktac-blink 2s step-end infinite;font-size:140%}@media (max-width:991.98px){aside.navbar .btn-outline-white,header.navbar .btn-outline-white{border-color:hsla(0,0%,100%,.2)}}.ts-dropdown{z-index:1021}.ts-control>input{min-width:unset}.ts-dropdown .optgroup [data-selectable].option{padding-left:20px}.checkbox-menu.show{z-index:1021}.duration-widget .input-group{min-width:110px}.duration-widget input.duration-input{min-width:55px}#report-form div.btn-list{width:100%}#report-form div.btn-list div.selectpicker{max-width:400px;min-width:200px}#report-form div.btn-list li.dropdown-item .form-check{margin-bottom:0}.color-choice-item{border-radius:var(--tblr-border-radius);display:inline-block;height:20px;margin-right:10px;width:20px}fieldset:empty{display:none}.modal-content{box-shadow:0 10px 80px rgba(0,0,0,.6)}.progress-group .progress-title{white-space:nowrap;width:100%}.progress-group .progress-title .progress-text{display:inline-block;width:1%}.progress-group .progress-title .progress-number{display:inline-block;float:none;width:98%}.initials{font-size:13px;font-weight:700}.avatar-xs{height:23px;width:23px}.avatar-xs .initials{font-size:10px}.avatar-sm{height:38px;width:38px}.avatar-sm .initials{font-size:17px}.avatar-md{height:45px;width:45px}.avatar-md .initials{font-size:21px}.avatar-lg{height:60px;width:60px}.avatar-lg .initials{font-size:28px}.widget-user-image{border:3px solid #fff;border-radius:50%}.widget-user-image .avatar{height:83px;width:83px}.widget-user-image .avatar .initials{font-size:40px}table.dataTable.table>tbody>tr>td.avatars .avatar{margin:1px}.avatar-list-stacked .avatar{margin-right:calc(var(--tblr-avatar-size)*-.2)!important}[data-bs-theme=dark] table.dataTable th.today{color:var(--tblr-muted)}[data-bs-theme=dark] .overlay{background:rgba(0,0,0,.5)}[data-bs-theme=dark] .overlay>.fas{color:var(--tblr-white)}section.quick-entry-page #quick_entry_form{display:none}section.quick-entry-page .ts-wrapper{display:table;table-layout:fixed}section.quick-entry-page #quick_entry_box .duration-widget .input-group{min-width:85px}section.quick-entry-page #quick_entry_box .duration-widget .btn-duration-preset{padding:7px 8px 7px 5px}section.quick-entry-page #quick_entry_box .duration-widget input{padding:7px 3px 7px 7px}fieldset legend.col-form-label{padding-top:0}fieldset.form-fieldset legend{font-size:1rem;font-weight:700}.ts-dropdown .optgroup:before{--tblr-spacer:.5rem}.dropdown-divider{--tblr-dropdown-divider-margin-y:.5rem}.float-help{background-color:var(--tblr-body-color);border-radius:50px;bottom:20px;height:50px;position:fixed;right:20px;text-align:center;width:50px;z-index:1021}.float-help i{color:var(--tblr-body-bg);font-size:30px;margin-top:11px}@media (max-width:767.98px){.float-help{border-radius:36px;height:36px;width:36px}.float-help i{font-size:20px;margin-top:8px}}html[dir=rtl] .fa-chevron-left:before{content:"\f054"}html[dir=rtl] .fa-chevron-right:before{content:"\f053"} \ No newline at end of file +.inline-search{max-width:200px}.inline-search #searchTerm{border-right:0}@media (min-width:360px){.inline-search{max-width:235px}}@media (min-width:768px){.inline-search{max-width:325px}.search-dropdown{width:500px}}h1.navbar-brand a span{display:inline-block;padding-top:.5rem}h1.navbar-brand a:hover{text-decoration:none}#export-buttons .dropdown-menu,.inline-search .dropdown-menu,.page-actions .dropdown-menu{z-index:1021}.navbar{--tblr-navbar-brand-font-size:1rem}.error-page{margin-bottom:50px}@media print{.control-sidebar,.sf-toolbar{display:none!important}.col-print-12{width:100%}.content-wrapper{margin:0!important}section.content{margin:0;padding:0}span i.dot{display:none;margin-right:0!important}}.page-center{justify-content:center}.hidden-no-space{margin:0;padding:0}.pre-scrollable{max-height:340px;overflow-y:scroll}td.timesheet-description ul{padding-left:15px}td.timesheet-description p{margin:0}td.badges{line-height:22px}.open-edit{cursor:pointer}td.w-min,th.w-min{white-space:nowrap;width:1%}table.dataTable thead>tr>th.hw-min{white-space:normal;width:1%}.w-avatar{width:40px}.w-avatar img.avatar{max-width:40px}.list-group-item.danger{border-left:.2rem solid #d63939!important;padding-left:1.3rem}.list-group-item.success{border-left:.2rem solid #2fb344!important;padding-left:1.3rem}.box-body-scrollable{max-height:340px;overflow:auto}#activity_details_box th,#customer_details_box th,#project_details_box th{width:30%}.overlay{background:hsla(0,0%,100%,.7);height:100%;left:0;position:fixed;top:0;width:100%;z-index:1021}.overlay>.fas{color:#000;font-size:30px;left:50vw;margin-left:-15px;margin-top:-15px;position:absolute;top:50vh}table.dataTable{clear:both;margin-bottom:0!important;max-width:none!important}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer;position:relative}table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{font-weight:700}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after{font-family:Font Awesome\ 5 Free;font-size:12px;opacity:.5;padding-left:5px}table.dataTable thead .sorting:after{content:"\f0dc";opacity:.2}table.dataTable thead .sorting_asc:after{content:"\f077"}table.dataTable thead .sorting_desc:after{content:"\f078"}table.dataTable thead>tr>th{vertical-align:top;white-space:nowrap}table.dataTable th:active{outline:none}table.table-bordered thead td,table.table-bordered thead th{border-left-width:0;border-top-width:0}table.table-bordered tbody td,table.table-bordered tbody th,table.table-bordered tfoot td,table.table-bordered tfoot th{border-bottom-width:0;border-left-width:0}table.table-bordered td:last-child,table.table-bordered th:last-child{border-right-width:0}@media (min-width:1200px){table.dataTable.table>tbody>tr>td{vertical-align:middle}}table.dataTable .actions{width:40px}table.dataTable tr.summary td{background-color:var(--tblr-bg-surface-secondary);border:0;font-weight:700}table.dataTable tr th.multiCheckbox{width:15px}table.dataTable tr.overlapping{border-top:2px solid rgba(214,57,57,.1)}table.dataTable tr.exported{opacity:.7}table.dataTable td .btn-group{display:flex}table.dataTable td .label{font-size:85%;line-height:24px;padding:.3em .6em}table.dataTable td span.label-invisible{opacity:.6}table.dataTable td.weekend,table.dataTable th.weekend{background-color:var(--tblr-bg-surface-secondary)}table.dataTable th.today{background-color:var(--tblr-highlight-bg)}table.dataTable td.total,table.dataTable th.total{font-weight:700}.form-dataTable table.dataTable .form-group{margin-bottom:0}table.table-hover tr.exported:hover{opacity:1}.calendar-entry ul{list-style-type:none;padding:0}.calendar-entry div{white-space:pre-line}.draggable{cursor:grab}#timesheet_calendar{--bs-gray-400:var(--tblr-border-color);--bs-gray-200:var(--tblr-gray-200)}.fc-day-sat,.fc-day-sun{background-color:var(--tblr-bg-surface-secondary)}@keyframes ticktac-blink{0%{opacity:1}5%{opacity:.95}10%{opacity:.9}15%{opacity:.85}20%{opacity:.8}25%{opacity:.75}30%{opacity:.7}35%{opacity:.65}40%{opacity:.6}45%{opacity:.65}50%{opacity:.7}55%{opacity:.75}60%{opacity:.8}65%{opacity:.85}70%{opacity:.9}75%{opacity:.95}to{opacity:1}}.ticktac-start i,.ticktac-stop i{animation:ticktac-blink 2s step-end infinite;font-size:140%}@media (max-width:991.98px){aside.navbar .btn-outline-white,header.navbar .btn-outline-white{border-color:hsla(0,0%,100%,.2)}}.ts-dropdown{z-index:1021}.ts-control>input{min-width:unset}.ts-dropdown .optgroup [data-selectable].option{padding-left:20px}.checkbox-menu.show{z-index:1021}.duration-widget .input-group{min-width:110px}.duration-widget input.duration-input{min-width:55px}#report-form div.btn-list{width:100%}#report-form div.btn-list div.selectpicker{max-width:400px;min-width:200px}#report-form div.btn-list li.dropdown-item .form-check{margin-bottom:0}.color-choice-item{border-radius:var(--tblr-border-radius);display:inline-block;height:20px;margin-right:10px;width:20px}fieldset:empty{display:none}fieldset>.mb-3.row:last-child{margin-bottom:0!important}.form-fieldset-light{background-color:unset;border-left:none;border-radius:0;border-right:none;border-top:none;margin-bottom:0}.form-fieldset-light:last-child{border:none}.modal-content{box-shadow:0 10px 80px rgba(0,0,0,.6)}.progress-group .progress-title{white-space:nowrap;width:100%}.progress-group .progress-title .progress-text{display:inline-block;width:1%}.progress-group .progress-title .progress-number{display:inline-block;float:none;width:98%}.initials{font-size:13px;font-weight:700}.avatar-xs{height:23px;width:23px}.avatar-xs .initials{font-size:10px}.avatar-sm{height:38px;width:38px}.avatar-sm .initials{font-size:17px}.avatar-md{height:45px;width:45px}.avatar-md .initials{font-size:21px}.avatar-lg{height:60px;width:60px}.avatar-lg .initials{font-size:28px}.widget-user-image{border:3px solid #fff;border-radius:50%}.widget-user-image .avatar{height:83px;width:83px}.widget-user-image .avatar .initials{font-size:40px}table.dataTable.table>tbody>tr>td.avatars .avatar{margin:1px}.avatar-list-stacked .avatar{margin-right:calc(var(--tblr-avatar-size)*-.2)!important}[data-bs-theme=dark] table.dataTable th.today{color:var(--tblr-muted)}[data-bs-theme=dark] .overlay{background:rgba(0,0,0,.5)}[data-bs-theme=dark] .overlay>.fas{color:var(--tblr-white)}section.quick-entry-page #quick_entry_form{display:none}section.quick-entry-page .ts-wrapper{display:table;table-layout:fixed}section.quick-entry-page #quick_entry_box .duration-widget .input-group{min-width:85px}section.quick-entry-page #quick_entry_box .duration-widget .btn-duration-preset{padding:7px 8px 7px 5px}section.quick-entry-page #quick_entry_box .duration-widget input{padding:7px 3px 7px 7px}fieldset legend.col-form-label{padding-top:0}fieldset.form-fieldset legend{font-size:1rem;font-weight:700}.ts-dropdown .optgroup:before{--tblr-spacer:.5rem}.dropdown-divider{--tblr-dropdown-divider-margin-y:.5rem}.float-help{background-color:var(--tblr-body-color);border-radius:50px;bottom:20px;height:50px;position:fixed;right:20px;text-align:center;width:50px;z-index:1021}.float-help i{color:var(--tblr-body-bg);font-size:30px;margin-top:11px}@media (max-width:767.98px){.float-help{border-radius:36px;height:36px;width:36px}.float-help i{font-size:20px;margin-top:8px}}html[dir=rtl] .fa-chevron-left:before{content:"\f054"}html[dir=rtl] .fa-chevron-right:before{content:"\f053"} \ No newline at end of file diff --git a/public/build/entrypoints.json b/public/build/entrypoints.json index c07c8d09..d50e69ed 100644 --- a/public/build/entrypoints.json +++ b/public/build/entrypoints.json @@ -6,7 +6,7 @@ "/build/app.9acfcfc8.js" ], "css": [ - "/build/app.eb575c90.css" + "/build/app.405a508c.css" ] }, "export-pdf": { @@ -64,7 +64,7 @@ "integrity": { "/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV", "/build/app.9acfcfc8.js": "sha384-tzbF0uEmHghIo+D5IuKarLYkwB3AyrLMTEFqR5mqaXCP5VSOWx3t+ZfD81kd80T+", - "/build/app.eb575c90.css": "sha384-CT92SMS+AjbulH+hT+pOq4jicscpVYpl/acq388EdqruZe26icGl9sNq9xaYXH8l", + "/build/app.405a508c.css": "sha384-D8LqbrX2x0UHuxQasN8nli7f/tzVYlNeJleATqXW1o0Pjwycyo0t1jKRe1h1MeIV", "/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ", diff --git a/public/build/manifest.json b/public/build/manifest.json index 4354c928..59b961dd 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -1,5 +1,5 @@ { - "build/app.css": "/build/app.eb575c90.css", + "build/app.css": "/build/app.405a508c.css", "build/app.js": "/build/app.9acfcfc8.js", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.js": "/build/export-pdf.d367a32e.js", diff --git a/src/API/UserController.php b/src/API/UserController.php index 6624114b..786d410e 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -197,6 +197,7 @@ final class UserController extends BaseApiController 'include_roles' => $this->isGranted('roles', $profile), 'include_active_flag' => ($profile->getId() !== $this->getUser()->getId()), 'include_preferences' => $this->isGranted('preferences', $profile), + 'include_supervisor' => $this->isGranted('supervisor', $profile), ]); $form->setData($profile); diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index 3a597ddb..b288f3d1 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -316,7 +316,8 @@ final class ProfileController extends AbstractController 'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'method' => 'POST', 'include_active_flag' => ($user->getId() !== $this->getUser()->getId()), - 'include_preferences' => false, + 'include_preferences' => true, + 'include_supervisor' => $this->isGranted('supervisor', $user), ] ); } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 8f662eca..0862d4f6 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -233,6 +233,7 @@ final class UserController extends AbstractController 'method' => 'POST', 'include_active_flag' => true, 'include_preferences' => true, + 'include_supervisor' => $this->isGranted('supervisor_other_profile'), 'include_teams' => $this->isGranted('teams_other_profile'), 'include_roles' => $this->isGranted('roles_other_profile'), ]); diff --git a/src/Entity/User.php b/src/Entity/User.php index 9a311500..3b827fe4 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -128,15 +128,15 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas * * @var Collection|null */ - #[ORM\OneToMany(targetEntity: 'App\Entity\UserPreference', mappedBy: 'user', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist'])] private ?Collection $preferences = null; /** * List of all team memberships. * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\TeamMember', mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)] - #[ORM\JoinColumn(onDelete: 'CASCADE')] + #[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)] + #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['User_Entity'])] @@ -216,6 +216,12 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas private bool $totpEnabled = false; #[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])] private bool $systemAccount = false; + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)] + #[Serializer\Expose] + #[Serializer\Groups(['User_Entity'])] + #[OA\Property(ref: '#/components/schemas/User')] + private ?User $supervisor = null; use ColorTrait; @@ -1281,4 +1287,24 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d')) }; } + + public function isWorkDay(\DateTimeInterface $dateTime): bool + { + return $this->getWorkHoursForDay($dateTime) > 0; + } + + public function hasSupervisor(): bool + { + return $this->supervisor !== null; + } + + public function getSupervisor(): ?User + { + return $this->supervisor; + } + + public function setSupervisor(?User $supervisor): void + { + $this->supervisor = $supervisor; + } } diff --git a/src/Form/ColorTrait.php b/src/Form/ColorTrait.php index b20d0474..1c729fc5 100644 --- a/src/Form/ColorTrait.php +++ b/src/Form/ColorTrait.php @@ -16,18 +16,18 @@ use Symfony\Component\Form\FormEvents; trait ColorTrait { - protected function addColor(FormBuilderInterface $builder): void + protected function addColor(FormBuilderInterface $builder, bool $required = false): void { $builder ->add('color', ColorChoiceType::class, [ - 'required' => false, + 'required' => $required, ]) ; // this code exists only for backward compatibility $builder->addEventListener( FormEvents::PRE_SET_DATA, - function (FormEvent $event) { + function (FormEvent $event) use ($required) { if (!$event->getForm()->getConfig()->hasOption('choices')) { return; } @@ -42,7 +42,7 @@ trait ColorTrait } $event->getForm()->add('color', ColorChoiceType::class, [ - 'required' => false, + 'required' => $required, 'choices' => $choices, ]); } diff --git a/src/Form/UserCreateType.php b/src/Form/UserCreateType.php index 00241e39..e2780d10 100644 --- a/src/Form/UserCreateType.php +++ b/src/Form/UserCreateType.php @@ -23,47 +23,45 @@ class UserCreateType extends UserEditType { public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder - ->add('username', null, [ - 'label' => 'username', - 'required' => true, - 'attr' => [ - 'autofocus' => 'autofocus' - ], - ]) - ->add('plainPassword', RepeatedType::class, [ - 'required' => true, - 'type' => PasswordType::class, - 'first_options' => [ - 'label' => 'password', - 'attr' => ['autocomplete' => 'new-password'], - 'block_prefix' => 'secret' - ], - 'second_options' => [ - 'label' => 'password_repeat', - 'attr' => ['autocomplete' => 'new-password'], - 'block_prefix' => 'secret' - ], - ]); + $builder->add('username', null, [ + 'label' => 'username', + 'required' => true, + 'attr' => [ + 'autofocus' => 'autofocus' + ], + ]); + + $builder->add('plainPassword', RepeatedType::class, [ + 'required' => true, + 'type' => PasswordType::class, + 'first_options' => [ + 'label' => 'password', + 'attr' => ['autocomplete' => 'new-password'], + 'block_prefix' => 'secret' + ], + 'second_options' => [ + 'label' => 'password_repeat', + 'attr' => ['autocomplete' => 'new-password'], + 'block_prefix' => 'secret' + ], + ]); parent::buildForm($builder, $options); if ($options['include_teams'] === true) { - $builder - ->add('teams', TeamType::class, [ - 'multiple' => true, - 'expanded' => false, - 'required' => false, - ]); + $builder->add('teams', TeamType::class, [ + 'multiple' => true, + 'expanded' => false, + 'required' => false, + ]); } if ($options['include_roles'] === true) { - $builder - ->add('roles', UserRoleType::class, [ - 'multiple' => true, - 'expanded' => false, - 'required' => false, - ]); + $builder->add('roles', UserRoleType::class, [ + 'multiple' => true, + 'expanded' => false, + 'required' => false, + ]); } } diff --git a/src/Form/UserEditType.php b/src/Form/UserEditType.php index be6d0a95..fd1e3667 100644 --- a/src/Form/UserEditType.php +++ b/src/Form/UserEditType.php @@ -12,9 +12,10 @@ namespace App\Form; use App\Configuration\SystemConfiguration; use App\Entity\User; use App\Form\Type\AvatarType; -use App\Form\Type\LanguageType; use App\Form\Type\MailType; use App\Form\Type\TimezoneType; +use App\Form\Type\UserLanguageType; +use App\Form\Type\UserType; use App\Form\Type\YesNoType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -35,20 +36,26 @@ class UserEditType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder - ->add('alias', TextType::class, [ - 'label' => 'alias', - 'required' => false, - ]) - ->add('title', TextType::class, [ - 'label' => 'title', - 'required' => false, - ]) - ->add('accountNumber', TextType::class, [ - 'label' => 'account_number', - 'required' => false, - ]) - ; + /** @var User|null $user */ + $user = null; + if (\array_key_exists('data', $options)) { + $user = $options['data']; + } + + $builder->add('alias', TextType::class, [ + 'label' => 'alias', + 'required' => false, + ]); + + $builder->add('title', TextType::class, [ + 'label' => 'title', + 'required' => false, + ]); + + $builder->add('accountNumber', TextType::class, [ + 'label' => 'account_number', + 'required' => false, + ]); if ($this->configuration->isThemeAllowAvatarUrls()) { $builder->add('avatar', AvatarType::class, [ @@ -61,7 +68,7 @@ class UserEditType extends AbstractType $builder->add('email', MailType::class); if ($options['include_preferences']) { - $builder->add('language', LanguageType::class, [ + $builder->add('language', UserLanguageType::class, [ 'required' => true, ]); @@ -75,12 +82,20 @@ class UserEditType extends AbstractType 'label' => 'active', 'help' => 'active.help' ]); + + $builder->add('systemAccount', YesNoType::class, [ + 'label' => 'system_account', + 'help' => 'system_account.help', + ]); } - $builder->add('systemAccount', YesNoType::class, [ - 'label' => 'system_account', - 'help' => 'system_account.help', - ]); + if ($options['include_supervisor']) { + $builder->add('supervisor', UserType::class, [ + 'required' => false, + 'label' => 'supervisor', + 'ignore_users' => ($user instanceof User && $user->getId() !== null ? [$user] : []), + ]); + } } public function configureOptions(OptionsResolver $resolver): void @@ -93,6 +108,7 @@ class UserEditType extends AbstractType 'csrf_token_id' => 'edit_user_profile', 'include_active_flag' => true, 'include_preferences' => true, + 'include_supervisor' => true, ]); } } diff --git a/src/Voter/UserVoter.php b/src/Voter/UserVoter.php index b240aed1..18e9c1a9 100644 --- a/src/Voter/UserVoter.php +++ b/src/Voter/UserVoter.php @@ -34,6 +34,7 @@ final class UserVoter extends Voter 'hourly-rate', 'view_team_member', 'contract', + 'supervisor', ]; public function __construct(private RolePermissionManager $permissionManager) diff --git a/templates/user/create.html.twig b/templates/user/create.html.twig index b8b4f4ad..5d0c7ad5 100644 --- a/templates/user/create.html.twig +++ b/templates/user/create.html.twig @@ -10,27 +10,12 @@ {% embed formEditTemplate with formOptions %} {% block form_body %}
-
+
{{ form_row(form.username) }}
-
+
{{ form_row(form.email) }}
-
- {{ form_row(form.color) }} -
-
- -
-
- {{ form_row(form.alias) }} -
-
- {{ form_row(form.title) }} -
-
- {{ form_row(form.accountNumber) }} -
@@ -42,6 +27,26 @@
+
+
+ {{ form_row(form.alias) }} +
+
+ {{ form_row(form.title) }} +
+
+ +
+
+ {{ form_row(form.color) }} +
+ {% if form.avatar is defined %} +
+ {{ form_row(form.avatar) }} +
+ {% endif %} +
+ {% if form.language is defined and form.timezone is defined %}
@@ -53,6 +58,15 @@
{% endif %} +
+
+ {{ form_row(form.accountNumber) }} +
+
+ {{ form_row(form.supervisor) }} +
+
+ {% if form.teams is defined or form.roles is defined %} {% set length = 12 %} {% if form.teams is defined and form.roles is defined %} diff --git a/templates/user/profile.html.twig b/templates/user/profile.html.twig index c2488337..99e9d941 100644 --- a/templates/user/profile.html.twig +++ b/templates/user/profile.html.twig @@ -1,20 +1,36 @@ {% extends 'user/form.html.twig' %} -{% block form_pre_content %} -
-
- {{ form_row(form.alias) }} -
-
- {{ form_row(form.color) }} -
-
-
-
- {{ form_row(form.title) }} -
-
- {{ form_row(form.accountNumber) }} -
-
+{% block form_content %} + + {% form_theme form 'form/horizontal.html.twig' %} + + {{ form_row(form._token) }} + +
+ {{ form_row(form.alias) }} + {{ form_row(form.email) }} + {% if form.systemAccount is defined %} + {{ form_row(form.systemAccount) }} + {% endif %} +
+ +
+ {{ form_row(form.accountNumber) }} + {% if form.supervisor is defined %} + {{ form_row(form.supervisor) }} + {% endif %} +
+ +
+ {{ form_row(form.title) }} + {{ form_row(form.color) }} + {% if form.avatar is defined %} + {{ form_row(form.avatar) }} + {% endif %} +
+ +
+ {{- form_rest(form) -}} +
+ {% endblock %} diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php index 707d1027..321fb845 100644 --- a/tests/API/APIControllerBaseTest.php +++ b/tests/API/APIControllerBaseTest.php @@ -78,7 +78,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest $this->assertEquals($pageSize, $response->headers->get('X-Per-Page')); } - protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, $method = 'GET'): void + protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, string $method = 'GET'): void { $this->request($client, $url, $method); $this->assertResponseIsSecured($client->getResponse(), $url); @@ -126,7 +126,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest ]); } - public function request(HttpKernelBrowser $client, string $url, $method = 'GET', array $parameters = [], string $content = null): Crawler + public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler { $server = ['HTTP_CONTENT_TYPE' => 'application/json', 'CONTENT_TYPE' => 'application/json']; @@ -355,6 +355,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest 'enabled' => 'bool', 'alias' => '@string', 'title' => '@string', + 'supervisor' => ['result' => 'object', 'type' => '@UserEntity'], 'avatar' => '@string', 'color' => '@string', 'teams' => ['result' => 'array', 'type' => 'Team'], diff --git a/tests/API/ApiDocControllerTest.php b/tests/API/ApiDocControllerTest.php index 3e4e67c7..3362f18d 100644 --- a/tests/API/ApiDocControllerTest.php +++ b/tests/API/ApiDocControllerTest.php @@ -17,12 +17,12 @@ use App\Tests\Controller\ControllerBaseTest; */ class ApiDocControllerTest extends ControllerBaseTest { - public function testIsSecure() + public function testIsSecure(): void { $this->assertUrlIsSecured('/api/doc'); } - public function testGetDocs() + public function testGetDocs(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/doc'); @@ -47,7 +47,7 @@ class ApiDocControllerTest extends ControllerBaseTest self::assertEquals($expectedKeys, $actual, sprintf('Expected %s sections in API docs, but found %s.', \count($actual), \count($expectedKeys))); } - public function testGetJsonDocs() + public function testGetJsonDocs(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/doc.json'); diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php index d17a00f2..cd16edd3 100644 --- a/tests/API/UserControllerTest.php +++ b/tests/API/UserControllerTest.php @@ -16,12 +16,15 @@ use App\Entity\User; */ class UserControllerTest extends APIControllerBaseTest { - public function testIsSecure() + public function testIsSecure(): void { $this->assertUrlIsSecured('/api/users'); } - public function getRoleTestData() + /** + * @return array> + */ + public function getRoleTestData(): array { return [ [User::ROLE_USER], @@ -33,12 +36,12 @@ class UserControllerTest extends APIControllerBaseTest /** * @dataProvider getRoleTestData */ - public function testIsSecureForRole(string $role) + public function testIsSecureForRole(string $role): void { $this->assertUrlIsSecuredForRole($role, '/api/users'); } - public function testGetCollection() + public function testGetCollection(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/api/users'); @@ -52,7 +55,7 @@ class UserControllerTest extends APIControllerBaseTest } } - public function testGetCollectionWithQuery() + public function testGetCollectionWithQuery(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 2, 'orderBy' => 'email', 'order' => 'DESC', 'term' => 'chris']); @@ -66,7 +69,7 @@ class UserControllerTest extends APIControllerBaseTest } } - public function testGetCollectionWithQuery2() + public function testGetCollectionWithQuery2(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 3, 'orderBy' => 'email', 'order' => 'DESC']); @@ -80,7 +83,7 @@ class UserControllerTest extends APIControllerBaseTest } } - public function testGetEntity() + public function testGetEntity(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/api/users/1'); @@ -93,7 +96,7 @@ class UserControllerTest extends APIControllerBaseTest self::assertEquals('Clara Haynes', $result['alias']); } - public function testGetMyProfile() + public function testGetMyProfile(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/api/users/me'); @@ -106,18 +109,18 @@ class UserControllerTest extends APIControllerBaseTest self::assertEquals('', $result['alias']); } - public function testNotFound() + public function testNotFound(): void { $this->assertEntityNotFound(User::ROLE_SUPER_ADMIN, '/api/users/99'); } - public function testGetEntityAccessDenied() + public function testGetEntityAccessDenied(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertApiAccessDenied($client, '/api/users/4', 'You are not allowed to view this profile'); } - public function testGetEntityAccessAllowedForOwnProfile() + public function testGetEntityAccessAllowedForOwnProfile(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/users/2'); @@ -127,7 +130,7 @@ class UserControllerTest extends APIControllerBaseTest self::assertApiResponseTypeStructure('UserEntity', $result); } - public function testPostAction() + public function testPostAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $data = [ @@ -136,6 +139,7 @@ class UserControllerTest extends APIControllerBaseTest 'title' => 'asdfghjkl', 'plainPassword' => 'foo@example.com', 'enabled' => true, + 'supervisor' => 2, 'language' => 'ru', 'timezone' => 'Europe/Paris', 'roles' => [ @@ -158,7 +162,7 @@ class UserControllerTest extends APIControllerBaseTest self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN'], $result['roles']); } - public function testPostActionWithShortPassword() + public function testPostActionWithShortPassword(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $data = [ @@ -181,7 +185,7 @@ class UserControllerTest extends APIControllerBaseTest $this->assertApiCallValidationError($response, ['plainPassword']); } - public function testPostActionWithValidationErrors() + public function testPostActionWithValidationErrors(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $data = [ @@ -201,7 +205,7 @@ class UserControllerTest extends APIControllerBaseTest $this->assertApiCallValidationError($response, ['username', 'email', 'plainPassword', 'language', 'timezone', 'roles']); } - public function testPostActionWithInvalidUser() + public function testPostActionWithInvalidUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $data = [ @@ -217,7 +221,7 @@ class UserControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($response, 'Access denied.'); } - public function testPatchAction() + public function testPatchAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $data = [ @@ -261,19 +265,19 @@ class UserControllerTest extends APIControllerBaseTest self::assertEquals(['ROLE_TEAMLEAD'], $result['roles']); } - public function testPatchActionWithUnknownUser() + public function testPatchActionWithUnknownUser(): void { $this->assertEntityNotFoundForPatch(User::ROLE_SUPER_ADMIN, '/api/users/255', []); } - public function testPatchActionWithInvalidUser() + public function testPatchActionWithInvalidUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->request($client, '/api/users/1', 'PATCH', [], json_encode(['language' => 'hu'])); $this->assertApiResponseAccessDenied($client->getResponse(), 'Not allowed to edit user'); } - public function testPatchActionWithValidationErrors() + public function testPatchActionWithValidationErrors(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $data = [ diff --git a/tests/Controller/ActivityControllerTest.php b/tests/Controller/ActivityControllerTest.php index 339437e8..969d1e0b 100644 --- a/tests/Controller/ActivityControllerTest.php +++ b/tests/Controller/ActivityControllerTest.php @@ -203,6 +203,7 @@ class ActivityControllerTest extends ControllerBaseTest ]); $location = $this->assertIsModalRedirect($client, '/details'); + self::assertNotNull($location); $this->requestPure($client, $location); $this->assertDetailsPage($client); diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php index 62c7a95c..3b7a0a14 100644 --- a/tests/Controller/ControllerBaseTest.php +++ b/tests/Controller/ControllerBaseTest.php @@ -17,6 +17,7 @@ use App\Form\Type\DateRangeType; use App\Repository\UserRepository; use App\Tests\KernelTestTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -77,7 +78,7 @@ abstract class ControllerBaseTest extends WebTestCase return self::getContainer()->get($service); } - protected function loadUserFromDatabase(string $username) + protected function loadUserFromDatabase(string $username): User { /** @var UserRepository $userRepository */ $userRepository = self::getContainer()->get('doctrine')->getRepository(User::class); @@ -102,7 +103,7 @@ abstract class ControllerBaseTest extends WebTestCase $this->clearConfigCache(); } - protected function clearConfigCache() + protected function clearConfigCache(): void { /** @var ConfigurationService $service */ $service = self::getContainer()->get(ConfigurationService::class); @@ -146,30 +147,17 @@ abstract class ControllerBaseTest extends WebTestCase return $url; } - /** - * @param HttpKernelBrowser $client - * @param string $url - * @param string $method - * @param array $parameters - * @param string $content - * @return \Symfony\Component\DomCrawler\Crawler - */ - public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null) + public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler { return $client->request($method, $this->createUrl($url), $parameters, [], [], $content); } - public function requestPure(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null) + public function requestPure(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler { return $client->request($method, $url, $parameters, [], [], $content); } - /** - * @param HttpKernelBrowser $client - * @param string $url - * @param string $method - */ - protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, ?string $method = 'GET') + protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, string $method = 'GET'): void { $this->request($client, $url, $method); @@ -189,7 +177,7 @@ abstract class ControllerBaseTest extends WebTestCase ); } - protected function assertSuccessResponse(HttpKernelBrowser $client, string $message = '') + protected function assertSuccessResponse(HttpKernelBrowser $client, string $message = ''): void { $response = $client->getResponse(); self::assertThat($response, new ResponseConstraint\ResponseIsSuccessful(), 'Response is not successful, got code: ' . $response->getStatusCode()); @@ -225,19 +213,19 @@ abstract class ControllerBaseTest extends WebTestCase ); } - protected function assertAccessIsGranted(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = []) + protected function assertAccessIsGranted(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = []): void { $this->request($client, $url, $method, $parameters); self::assertTrue($client->getResponse()->isSuccessful()); } - protected function assertRouteNotFound(HttpKernelBrowser $client) + protected function assertRouteNotFound(HttpKernelBrowser $client): void { self::assertFalse($client->getResponse()->isSuccessful()); self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode()); } - protected function assert404(Response $response, ?string $message = null) + protected function assert404(Response $response, ?string $message = null): void { $message = 'Page not found'; self::assertFalse($response->isSuccessful()); @@ -245,7 +233,7 @@ abstract class ControllerBaseTest extends WebTestCase self::assertStringContainsString($message, $response->getContent()); } - protected function assertMainContentClass(HttpKernelBrowser $client, string $classname) + protected function assertMainContentClass(HttpKernelBrowser $client, string $classname): void { self::assertStringContainsString('
', $client->getResponse()->getContent()); } @@ -253,7 +241,7 @@ abstract class ControllerBaseTest extends WebTestCase /** * @param HttpKernelBrowser $client */ - protected function assertHasDataTable(HttpKernelBrowser $client) + protected function assertHasDataTable(HttpKernelBrowser $client): void { self::assertStringContainsString('getCrawler()->filter('section.content div.' . $class . ' table.dataTable tbody tr:not(.summary)'); self::assertEquals($count, $node->count()); @@ -284,7 +272,7 @@ abstract class ControllerBaseTest extends WebTestCase * @param HttpKernelBrowser $client * @param array $buttons */ - protected function assertPageActions(HttpKernelBrowser $client, array $buttons) + protected function assertPageActions(HttpKernelBrowser $client, array $buttons): void { $node = $client->getCrawler()->filter('div.page-header div.page-actions .pa-desktop a'); @@ -307,7 +295,7 @@ abstract class ControllerBaseTest extends WebTestCase * @param array $fieldNames array of form-fields that should fail * @param bool $disableValidation whether the form should validate before submitting or not */ - protected function assertHasValidationError(HttpKernelBrowser $client, $url, $formSelector, array $formData, array $fieldNames, $disableValidation = true) + protected function assertHasValidationError(HttpKernelBrowser $client, string $url, string $formSelector, array $formData, array $fieldNames, bool $disableValidation = true): void { $crawler = $client->request('GET', $this->createUrl($url)); $form = $crawler->filter($formSelector)->form(); @@ -342,24 +330,16 @@ abstract class ControllerBaseTest extends WebTestCase } } - /** - * @param string $role the USER role to use for the request - * @param string $url the URL of the page displaying the initial form to submit - * @param string $formSelector a selector to find the form to test - * @param array $formData values to fill in the form - * @param array $fieldNames array of form-fields that should fail - * @param bool $disableValidation whether the form should validate before submitting or not - */ - protected function assertFormHasValidationError($role, $url, $formSelector, array $formData, array $fieldNames, $disableValidation = true) + protected function assertFormHasValidationError(string $role, string $url, string $formSelector, array $formData, array $fieldNames): void { $client = $this->getClientForAuthenticatedUser($role); - $this->assertHasValidationError($client, $url, $formSelector, $formData, $fieldNames, $disableValidation); + $this->assertHasValidationError($client, $url, $formSelector, $formData, $fieldNames); } /** * @param HttpKernelBrowser $client */ - protected function assertHasNoEntriesWithFilter(HttpKernelBrowser $client) + protected function assertHasNoEntriesWithFilter(HttpKernelBrowser $client): void { $this->assertCalloutWidgetWithMessage($client, 'No entries were found based on your selected filters.'); } @@ -368,18 +348,18 @@ abstract class ControllerBaseTest extends WebTestCase * @param HttpKernelBrowser $client * @param string $message */ - protected function assertCalloutWidgetWithMessage(HttpKernelBrowser $client, string $message) + protected function assertCalloutWidgetWithMessage(HttpKernelBrowser $client, string $message): void { $node = $client->getCrawler()->filter('div.alert.alert-warning.alert-important'); self::assertStringContainsString($message, $node->text(null, true)); } - protected function assertHasFlashDeleteSuccess(HttpKernelBrowser $client) + protected function assertHasFlashDeleteSuccess(HttpKernelBrowser $client): void { $this->assertHasFlashSuccess($client, 'Entry was deleted'); } - protected function assertHasFlashSaveSuccess(HttpKernelBrowser $client) + protected function assertHasFlashSaveSuccess(HttpKernelBrowser $client): void { $this->assertHasFlashSuccess($client, 'Saved changes'); } @@ -388,7 +368,7 @@ abstract class ControllerBaseTest extends WebTestCase * @param HttpKernelBrowser $client * @param string|null $message */ - protected function assertHasFlashSuccess(HttpKernelBrowser $client, string $message = null) + protected function assertHasFlashSuccess(HttpKernelBrowser $client, string $message = null): void { $this->assertHasFlashMessage($client, 'success', $message); } @@ -397,12 +377,12 @@ abstract class ControllerBaseTest extends WebTestCase * @param HttpKernelBrowser $client * @param string|null $message */ - protected function assertHasFlashError(HttpKernelBrowser $client, string $message = null) + protected function assertHasFlashError(HttpKernelBrowser $client, string $message = null): void { $this->assertHasFlashMessage($client, 'error', $message); } - private function assertHasFlashMessage(HttpKernelBrowser $client, string $type, string $message = null) + private function assertHasFlashMessage(HttpKernelBrowser $client, string $type, string $message = null): void { $content = $client->getResponse()->getContent(); self::assertStringContainsString('ALERT.' . $type . '(\'', $content, 'Could not find flash ' . $type . ' message'); @@ -418,7 +398,7 @@ abstract class ControllerBaseTest extends WebTestCase * @param HttpKernelBrowser $client * @param string $url */ - protected function assertIsRedirect(HttpKernelBrowser $client, ?string $url = null, bool $endsWith = true) + protected function assertIsRedirect(HttpKernelBrowser $client, ?string $url = null, bool $endsWith = true): void { self::assertResponseRedirects(); @@ -434,6 +414,7 @@ abstract class ControllerBaseTest extends WebTestCase self::assertEquals(201, $client->getResponse()->getStatusCode()); self::assertTrue($client->getResponse()->headers->has('x-modal-redirect'), 'Could not find "x-modal-redirect" header'); $location = $client->getResponse()->headers->get('x-modal-redirect'); + self::assertNotNull($location); // check for meta refresh $expectedMeta = sprintf('', $location); @@ -450,14 +431,11 @@ abstract class ControllerBaseTest extends WebTestCase return $location; } - protected function assertRedirectUrl(HttpKernelBrowser $client, ?string $url = null, bool $endsWith = true) + protected function assertRedirectUrl(HttpKernelBrowser $client, string $url, bool $endsWith = true): void { self::assertTrue($client->getResponse()->headers->has('Location'), 'Could not find "Location" header'); $location = $client->getResponse()->headers->get('Location'); - - if ($url === null) { - return; - } + self::assertNotNull($location); if ($endsWith) { self::assertStringEndsWith($url, $location, 'Redirect URL does not match'); @@ -466,18 +444,21 @@ abstract class ControllerBaseTest extends WebTestCase } } - protected function assertExcelExportResponse(HttpKernelBrowser $client, string $prefix) + protected function assertExcelExportResponse(HttpKernelBrowser $client, string $prefix): void { /** @var BinaryFileResponse $response */ $response = $client->getResponse(); self::assertInstanceOf(BinaryFileResponse::class, $response); + $disposition = $response->headers->get('Content-Disposition'); + self::assertNotNull($disposition); + self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type')); - self::assertStringContainsString('attachment; filename=' . $prefix, $response->headers->get('Content-Disposition')); - self::assertStringContainsString('.xlsx', $response->headers->get('Content-Disposition')); + self::assertStringContainsString('attachment; filename=' . $prefix, $disposition); + self::assertStringContainsString('.xlsx', $disposition); } - protected function assertInvalidCsrfToken(HttpKernelBrowser $client, string $url, string $expectedRedirect) + protected function assertInvalidCsrfToken(HttpKernelBrowser $client, string $url, string $expectedRedirect): void { $this->request($client, $url); diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index fceef609..32efd721 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -34,7 +34,7 @@ class PermissionControllerTest extends ControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertHasDataTable($client); - $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 130); + $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 132); $this->assertPageActions($client, [ 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), ]); diff --git a/tests/Controller/Security/SecurityControllerTest.php b/tests/Controller/Security/SecurityControllerTest.php index aeb3560c..8626bc83 100644 --- a/tests/Controller/Security/SecurityControllerTest.php +++ b/tests/Controller/Security/SecurityControllerTest.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; */ class SecurityControllerTest extends ControllerBaseTest { - public function testRootUrlIsRedirectedToLogin() + public function testRootUrlIsRedirectedToLogin(): void { $client = self::createClient(); $client->request('GET', '/'); @@ -35,7 +35,7 @@ class SecurityControllerTest extends ControllerBaseTest $this->assertIsRedirect($client, $this->createUrl('/login')); } - public function testLoginPageIsRendered() + public function testLoginPageIsRendered(): void { $client = self::createClient(); $this->request($client, '/login'); @@ -54,7 +54,7 @@ class SecurityControllerTest extends ControllerBaseTest $this->assertStringNotContainsString('Register a new account', $content); } - public function testLoginPositive() + public function testLoginPositive(): void { $client = self::createClient(); $this->request($client, '/login'); @@ -79,7 +79,7 @@ class SecurityControllerTest extends ControllerBaseTest $this->assertTrue($client->getResponse()->isSuccessful()); } - public function testLoginAlreadyLoggedIn() + public function testLoginAlreadyLoggedIn(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); @@ -94,7 +94,7 @@ class SecurityControllerTest extends ControllerBaseTest $this->assertTrue($client->getResponse()->isSuccessful()); } - public function testLoginNegative() + public function testLoginNegative(): void { $client = self::createClient(); $this->request($client, '/login'); @@ -114,7 +114,7 @@ class SecurityControllerTest extends ControllerBaseTest self::assertStringContainsString('
Invalid credentials.
', $client->getResponse()->getContent()); } - public function testCheckAction() + public function testCheckAction(): void { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.'); @@ -127,7 +127,7 @@ class SecurityControllerTest extends ControllerBaseTest $sut->checkAction(); } - public function testLogoutAction() + public function testLogoutAction(): void { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('You must activate the logout in your security firewall configuration.'); diff --git a/tests/Controller/SystemConfigurationControllerTest.php b/tests/Controller/SystemConfigurationControllerTest.php index 7d4dab69..1184fad1 100644 --- a/tests/Controller/SystemConfigurationControllerTest.php +++ b/tests/Controller/SystemConfigurationControllerTest.php @@ -186,8 +186,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest [ '#system_configuration_form_timesheet_configuration_0_value', // mode '#system_configuration_form_timesheet_configuration_6_value', // hard_limit - ], - true + ] ); } @@ -297,8 +296,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest '#system_configuration_form_customer_configuration_0_value', '#system_configuration_form_customer_configuration_1_value', '#system_configuration_form_customer_configuration_2_value', - ], - true + ] ); } @@ -344,8 +342,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest ], [ '#system_configuration_form_theme_configuration_1_value', - ], - true + ] ); } @@ -413,8 +410,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest '#system_configuration_form_calendar_configuration_3_value', '#system_configuration_form_calendar_configuration_4_value', '#system_configuration_form_calendar_configuration_5_value', - ], - true + ] ); } } diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index 6862861e..f7249ed0 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -27,7 +27,7 @@ use Symfony\Component\Security\Core\User\UserInterface; */ class UserTest extends TestCase { - public function testDefaultValues() + public function testDefaultValues(): void { $user = new User(); self::assertInstanceOf(EquatableInterface::class, $user); @@ -83,6 +83,8 @@ class UserTest extends TestCase self::assertEquals(0, $user->getHolidaysPerYear()); self::assertFalse($user->hasWorkHourConfiguration()); self::assertNull($user->getPublicHolidayGroup()); + self::assertFalse($user->hasSupervisor()); + self::assertNull($user->getSupervisor()); } public function testWorkContract(): void @@ -139,7 +141,7 @@ class UserTest extends TestCase self::assertTrue($sut->hasColor()); } - public function testWizards() + public function testWizards(): void { $sut = new User(); // internal name may not be changed @@ -155,7 +157,7 @@ class UserTest extends TestCase self::assertTrue($sut->hasSeenWizard('profile')); } - public function testAuth() + public function testAuth(): void { $user = new User(); @@ -183,7 +185,7 @@ class UserTest extends TestCase self::assertTrue($user->isInternalUser()); } - public function testDatetime() + public function testDatetime(): void { $date = new \DateTime('+1 day'); $user = new User(); @@ -191,7 +193,7 @@ class UserTest extends TestCase self::assertEquals($date, $user->getRegisteredAt()); } - public function testPasswordRequestedAt() + public function testPasswordRequestedAt(): void { $date = new \DateTime('-60 minutes'); $sut = new User(); @@ -205,7 +207,7 @@ class UserTest extends TestCase self::assertTrue($sut->isPasswordRequestNonExpired(3610)); } - public function testPreferences() + public function testPreferences(): void { $user = new User(); self::assertNull($user->getPreference('test')); @@ -228,7 +230,7 @@ class UserTest extends TestCase self::assertTrue($user->isExportDecimal()); } - public function testDisplayName() + public function testDisplayName(): void { $user = new User(); @@ -244,7 +246,7 @@ class UserTest extends TestCase self::assertEquals('foo', (string) $user); } - public function testGetUsername() + public function testGetUsername(): void { $user = new User(); @@ -260,7 +262,7 @@ class UserTest extends TestCase self::assertEquals('foo', (string) $user); } - public function testGetLocale() + public function testGetLocale(): void { $sut = new User(); self::assertEquals(User::DEFAULT_LANGUAGE, $sut->getLocale()); @@ -271,7 +273,7 @@ class UserTest extends TestCase self::assertEquals('fr', $sut->getLocale()); } - public function testTeams() + public function testTeams(): void { $sut = new User(); $user = new User(); @@ -314,7 +316,7 @@ class UserTest extends TestCase self::assertFalse($sut->hasTeamAssignment()); } - public function testRoles() + public function testRoles(): void { $sut = new User(); self::assertFalse($sut->canSeeAllData()); @@ -357,7 +359,7 @@ class UserTest extends TestCase * * @see #1562 */ - public function testMaxLength() + public function testMaxLength(): void { $sut = new User(); $sut->setAlias('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); @@ -372,7 +374,7 @@ class UserTest extends TestCase self::assertEquals(50, mb_strlen($sut->getTitle())); } - public function testPreferencesCollectionIsCreatedOnBrokenUser() + public function testPreferencesCollectionIsCreatedOnBrokenUser(): void { // this code is only used in some rare edge cases, maybe even only in development ... // lets keep it, as it occured during the work on SAML authentication @@ -390,7 +392,7 @@ class UserTest extends TestCase self::assertEquals('foobar', $sut->getPreferenceValue('test')); } - public function testCanSeeAllData() + public function testCanSeeAllData(): void { $sut = new User(); $sut->addRole(User::ROLE_USER); @@ -400,7 +402,7 @@ class UserTest extends TestCase self::assertFalse($sut->initCanSeeAllData(true)); } - public function testSystemAccount() + public function testSystemAccount(): void { $sut = new User(); self::assertFalse($sut->isSystemAccount()); @@ -410,7 +412,7 @@ class UserTest extends TestCase self::assertFalse($sut->isSystemAccount()); } - public function testExportAnnotations() + public function testExportAnnotations(): void { $sut = new AnnotationExtractor(); @@ -449,7 +451,7 @@ class UserTest extends TestCase } } - public function testEqualsTo() + public function testEqualsTo(): void { $sut = new User(); $sut->setUserIdentifier('foo'); @@ -479,7 +481,7 @@ class UserTest extends TestCase self::assertTrue($sut2->isEqualTo($sut)); } - public function testSerialize() + public function testSerialize(): void { $sut = new User(); $sut->setPassword('ABC-1234567890'); @@ -509,7 +511,7 @@ class UserTest extends TestCase self::assertEquals($expected, $actual); } - public function testTeamMemberships() + public function testTeamMemberships(): void { $team = new Team('Foo'); @@ -564,7 +566,7 @@ class UserTest extends TestCase self::assertCount(1, $sut->getMemberships()); } - public function testTeamMembershipsException() + public function testTeamMembershipsException(): void { $this->expectException(\InvalidArgumentException::class); $sut = new User(); @@ -572,4 +574,19 @@ class UserTest extends TestCase $member->setUser(new User()); $sut->addMembership($member); } + + public function testSupervisor(): void + { + $user = new User(); + self::assertFalse($user->hasSupervisor()); + self::assertNull($user->getSupervisor()); + + $supervisor = new User(); + $supervisor->setTitle('Cool boss'); + + $user->setSupervisor($supervisor); + self::assertTrue($user->hasSupervisor()); + self::assertNotNull($user->getSupervisor()); + self::assertSame($supervisor, $user->getSupervisor()); + } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 4ef4edaf..d76a5b59 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -422,21 +422,6 @@ parameters: count: 1 path: API/ApiDocControllerTest.php - - - message: "#^Method App\\\\Tests\\\\API\\\\ApiDocControllerTest\\:\\:testGetDocs\\(\\) has no return type specified\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ApiDocControllerTest\\:\\:testGetJsonDocs\\(\\) has no return type specified\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ApiDocControllerTest\\:\\:testIsSecure\\(\\) has no return type specified\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" count: 2 @@ -1252,101 +1237,6 @@ parameters: count: 1 path: API/UserControllerTest.php - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:getRoleTestData\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetCollection\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetCollectionWithQuery\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetCollectionWithQuery2\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetEntity\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetEntityAccessAllowedForOwnProfile\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetEntityAccessDenied\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testGetMyProfile\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testIsSecure\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testIsSecureForRole\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPatchAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPatchActionWithInvalidUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPatchActionWithUnknownUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPatchActionWithValidationErrors\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPostAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPostActionWithInvalidUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPostActionWithShortPassword\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\UserControllerTest\\:\\:testPostActionWithValidationErrors\\(\\) has no return type specified\\.$#" - count: 1 - path: API/UserControllerTest.php - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" count: 9 @@ -2402,41 +2292,11 @@ parameters: count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assert404\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertAccessIsGranted\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertAccessIsGranted\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#" count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertCalloutWidgetWithMessage\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertDataTableRowCount\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertExcelExportResponse\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertFormHasValidationError\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertFormHasValidationError\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#" count: 1 @@ -2447,51 +2307,6 @@ parameters: count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasDataTable\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasFlashDeleteSuccess\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasFlashError\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasFlashMessage\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasFlashSaveSuccess\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasFlashSuccess\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasNoEntriesWithFilter\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasProgressbar\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasValidationError\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertHasValidationError\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#" count: 1 @@ -2502,81 +2317,21 @@ parameters: count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertInvalidCsrfToken\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertIsModalRedirect\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertIsRedirect\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertMainContentClass\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertPageActions\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertPageActions\\(\\) has parameter \\$buttons with no value type specified in iterable type array\\.$#" count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertRedirectUrl\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertRequestIsSecured\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertRouteNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:assertSuccessResponse\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:clearConfigCache\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:importFixture\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:loadUserFromDatabase\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:request\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#" count: 1 path: Controller/ControllerBaseTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:requestPure\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:requestPure\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#" count: 1 @@ -2592,21 +2347,6 @@ parameters: count: 10 path: Controller/ControllerBaseTest.php - - - message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|null given\\.$#" - count: 4 - path: Controller/ControllerBaseTest.php - - - - message: "#^Parameter \\#2 \\$string of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringEndsWith\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: Controller/ControllerBaseTest.php - - - - message: "#^Parameter \\#3 \\$method of method App\\\\Tests\\\\Controller\\\\ControllerBaseTest\\:\\:request\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: Controller/ControllerBaseTest.php - - message: "#^Cannot call method addSubscriber\\(\\) on object\\|null\\.$#" count: 1 @@ -3537,41 +3277,6 @@ parameters: count: 6 path: Controller/Security/PasswordResetControllerTest.php - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testCheckAction\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testLoginAlreadyLoggedIn\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testLoginNegative\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testLoginPageIsRendered\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testLoginPositive\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testLogoutAction\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\Controller\\\\Security\\\\SecurityControllerTest\\:\\:testRootUrlIsRedirectedToLogin\\(\\) has no return type specified\\.$#" - count: 1 - path: Controller/Security/SecurityControllerTest.php - - message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#" count: 6 @@ -4722,106 +4427,6 @@ parameters: count: 1 path: Entity/UserTest.php - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testAuth\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testCanSeeAllData\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testDatetime\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testDisplayName\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testEqualsTo\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testExportAnnotations\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testGetLocale\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testGetUsername\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testMaxLength\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testPasswordRequestedAt\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testPreferences\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testPreferencesCollectionIsCreatedOnBrokenUser\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testRoles\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testSerialize\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testSystemAccount\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testTeamMemberships\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testTeamMembershipsException\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testTeams\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testWizards\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserTest.php - - message: "#^Parameter \\#1 \\$string of function mb_strlen expects string, string\\|null given\\.$#" count: 5 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 8ca914ea..61d2e70c 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1621,6 +1621,10 @@ day_halfHalber Tag + + supervisor + Vorgesetzter + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 8e2a6d57..a6bd2f16 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1621,6 +1621,10 @@ day_halfHalf day + + supervisor + Vorgesetzter +