diff --git a/src/Controller/Admin/User/EditController.php b/src/Controller/Admin/User/EditController.php index 90b8f43..fadba8d 100644 --- a/src/Controller/Admin/User/EditController.php +++ b/src/Controller/Admin/User/EditController.php @@ -45,6 +45,7 @@ class EditController extends AbstractController return $this->render('admin/user/modal_edit.html.twig', [ 'user' => $user, 'form' => $form, + 'syncedRoles' => Role::syncedOnly($user->getRoles()), ]); } @@ -66,6 +67,7 @@ class EditController extends AbstractController return $this->render('admin/user/modal_edit.html.twig', [ 'user' => $user, 'form' => $form, + 'syncedRoles' => Role::syncedOnly($user->getRoles()), ]); } diff --git a/src/Form/Admin/UserType.php b/src/Form/Admin/UserType.php index 8cf0b53..293b976 100644 --- a/src/Form/Admin/UserType.php +++ b/src/Form/Admin/UserType.php @@ -12,10 +12,12 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Lets an administrator assign roles and hotel codes to an existing account. + * Lets an administrator assign the privileged roles and the hotel codes of an existing account. * - * This is the only way those two get changed after the account was created — BpnAuthenticator - * imports them once and never touches them again. + * Only Role::PRIVILEGED is offered: the remaining roles are synced from the BusPro CRM on every + * login (see BpnAuthenticator), so editing them here would be undone at the user's next login. + * Hotel codes are seeded once at account creation, which makes this form the only way to change + * them afterwards. * * @extends AbstractType */ @@ -35,14 +37,16 @@ class UserType extends AbstractType $builder ->add('roles', ChoiceType::class, [ 'label' => 'Rollen', - 'choices' => array_flip(Role::labels()), + 'choices' => $this->privilegedChoices(), 'multiple' => true, 'expanded' => true, 'required' => false, - // User::getRoles() prepends the implicit ROLE_USER, which must not be written back. - 'getter' => static fn (User $user): array => array_values(array_diff($user->getRoles(), ['ROLE_USER'])), + 'help' => 'Alle übrigen Rollen kommen bei jeder Anmeldung aus BusPro und lassen sich hier nicht ändern.', + // Only the administrator-granted half is editable; the synced half is preserved, + // as is the implicit ROLE_USER, which must never be written back. + 'getter' => static fn (User $user): array => Role::privilegedOnly($user->getRoles()), 'setter' => static function (User $user, array $roles): void { - $user->setRoles(array_values(array_unique($roles))); + $user->setRoles(Role::combine($user->getRoles(), $roles)); }, ]) ->add('hotelCodes', ChoiceType::class, [ @@ -58,6 +62,14 @@ class UserType extends AbstractType ; } + /** + * @return array label => role + */ + private function privilegedChoices(): array + { + return array_flip(array_intersect_key(Role::labels(), array_flip(Role::PRIVILEGED))); + } + /** * Codes already stored on the account are always offered, even when they are missing from * the configured catalog — otherwise saving the form would silently drop them. diff --git a/templates/admin/user/modal_edit.html.twig b/templates/admin/user/modal_edit.html.twig index 8e3e7d8..3342d39 100644 --- a/templates/admin/user/modal_edit.html.twig +++ b/templates/admin/user/modal_edit.html.twig @@ -9,6 +9,9 @@
{{ user.email }}
+
+ aus BusPro: {{ syncedRoles | map_roles | join(', ') | default('–') }} +
{{ form_start(form) }}
{{ form_row(form.roles) }} diff --git a/tests/Form/Admin/UserTypeTest.php b/tests/Form/Admin/UserTypeTest.php index da491a9..77bf37d 100644 --- a/tests/Form/Admin/UserTypeTest.php +++ b/tests/Form/Admin/UserTypeTest.php @@ -13,29 +13,37 @@ use Symfony\Component\Form\Forms; class UserTypeTest extends TestCase { - public function testStoredRolesArePreselectedWithoutTheImplicitRoleUser(): void + public function testOnlyThePrivilegedRolesArePreselected(): void { - $user = (new User('teamer@example.org'))->setRoles([Role::TEAMER]); + $user = (new User('teamer@example.org'))->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]); - self::assertSame([Role::TEAMER], $this->createForm($user)->get('roles')->getData()); + self::assertSame([Role::GROUPS_MANAGER], $this->createForm($user)->get('roles')->getData()); } - public function testSubmittingRolesDoesNotStoreTheImplicitRoleUser(): void + public function testOnlyPrivilegedRolesAreOffered(): void + { + $choices = $this->createForm(new User('teamer@example.org'))->get('roles')->getConfig()->getOption('choices'); + + // The rest is synced from BusPro on every login and would be overwritten right away. + self::assertSame(Role::PRIVILEGED, array_values($choices)); + } + + public function testSubmittingRolesKeepsTheSyncedOnesAndNotTheImplicitRoleUser(): void { $user = (new User('teamer@example.org'))->setRoles([Role::TEAMER]); $form = $this->createForm($user); - $form->submit(['roles' => [Role::TEAMER, Role::GROUPS_MANAGER], 'hotelCodes' => []]); + $form->submit(['roles' => [Role::GROUPS_MANAGER], 'hotelCodes' => []]); self::assertTrue($form->isSynchronized()); // getRoles() prepends ROLE_USER; it must not have been persisted a second time. self::assertSame(['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $user->getRoles()); } - public function testClearingEveryCheckboxEmptiesTheAssignment(): void + public function testClearingEveryCheckboxKeepsTheSyncedRoles(): void { $user = (new User('teamer@example.org')) - ->setRoles([Role::TEAMER]) + ->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]) ->setHotelCodes(['SSL']) ; @@ -43,7 +51,7 @@ class UserTypeTest extends TestCase $form->submit([]); self::assertTrue($form->isSynchronized()); - self::assertSame(['ROLE_USER'], $user->getRoles()); + self::assertSame(['ROLE_USER', Role::TEAMER], $user->getRoles()); self::assertSame([], $user->getHotelCodes()); }