From 56a1a488dea20143fde32a610e1c3627495df2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 18 Apr 2024 15:44:05 +0200 Subject: [PATCH 1/7] Return the content of the ticket on replace motive POST request --- .../src/Controller/ReplaceMotiveController.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillTicketBundle/src/Controller/ReplaceMotiveController.php b/src/Bundle/ChillTicketBundle/src/Controller/ReplaceMotiveController.php index d6f1acf94..8af2773e8 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/ReplaceMotiveController.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/ReplaceMotiveController.php @@ -59,6 +59,11 @@ final readonly class ReplaceMotiveController $this->entityManager->flush(); - return new JsonResponse(null, Response::HTTP_CREATED); + return new JsonResponse( + $this->serializer->serialize($ticket, 'json', ['groups' => 'read']), + Response::HTTP_CREATED, + [], + true + ); } } From 613ee8b186c871b9268ed1489b260f1f614eb1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 18 Apr 2024 21:57:55 +0200 Subject: [PATCH 2/7] Add functionality to add comments to tickets A new controller, 'AddCommentController', has been added. This controller implements the 'AddCommentCommandHandler', allowing users to add comments to tickets. Additionally, corresponding test cases were implemented. The Ticket entity was also updated to accept and manage comments. API endpoint specs were updated to reflect these changes. --- .../ChillTicketBundle/chill.api.specs.yaml | 29 +++++ .../src/Action/Ticket/AddCommentCommand.php | 25 +++++ .../Handler/AddCommentCommandHandler.php | 31 ++++++ .../src/Controller/AddCommentController.php | 68 ++++++++++++ .../ChillTicketBundle/src/Entity/Comment.php | 8 +- .../ChillTicketBundle/src/Entity/Ticket.php | 13 ++- .../Handler/AddCommentCommandHandlerTest.php | 52 +++++++++ .../Controller/AddCommentControllerTest.php | 104 ++++++++++++++++++ 8 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/AddCommentCommand.php create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/AddCommentCommandHandler.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/AddCommentController.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AddCommentCommandHandlerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Controller/AddCommentControllerTest.php diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index ce53732ff..86049cc24 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -64,3 +64,32 @@ paths: description: "ACCEPTED" 422: description: "UNPROCESSABLE ENTITY" + + /1.0/ticket/{id}/comment/add: + post: + tags: + - ticket + summary: Add a comment to an existing ticket + parameters: + - name: id + in: path + required: true + description: The ticket id + schema: + type: integer + format: integer + minimum: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + responses: + 201: + description: "ACCEPTED" + 422: + description: "UNPROCESSABLE ENTITY" diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddCommentCommand.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddCommentCommand.php new file mode 100644 index 000000000..1c009cfc8 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddCommentCommand.php @@ -0,0 +1,25 @@ +content, $ticket); + + $this->entityManager->persist($comment); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Controller/AddCommentController.php b/src/Bundle/ChillTicketBundle/src/Controller/AddCommentController.php new file mode 100644 index 000000000..157e69698 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/AddCommentController.php @@ -0,0 +1,68 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only user can add ticket comments.'); + } + + $command = $this->serializer->deserialize($request->getContent(), AddCommentCommand::class, 'json', ['groups' => 'write']); + + $errors = $this->validator->validate($command); + + if (count($errors) > 0) { + return new JsonResponse( + $this->serializer->serialize($errors, 'json'), + Response::HTTP_UNPROCESSABLE_ENTITY, + [], + true + ); + } + + $this->addCommentCommandHandler->handle($ticket, $command); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($ticket, 'json', ['groups' => 'read']), + Response::HTTP_CREATED, + [], + true + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Comment.php b/src/Bundle/ChillTicketBundle/src/Entity/Comment.php index d62e6d8b4..de12d5d47 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Comment.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Comment.php @@ -31,12 +31,14 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface private ?int $id = null; public function __construct( + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] + private string $content, #[ORM\ManyToOne(targetEntity: Ticket::class, inversedBy: 'comments')] #[JoinColumn(nullable: false)] private Ticket $ticket, - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] - private string $content = '' - ) {} + ) { + $ticket->addComment($this); + } public function getId(): ?int { diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index de369379b..cd49d7377 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -104,16 +104,27 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface ->getValues(); } + /** + * @internal use @see{Comment::__construct} instead + */ + public function addComment(Comment $comment): void + { + $this->comments->add($comment); + } + /** * Add a PersonHistory. * - * This method should not be used, use @see{PersonHistory::__construct()} insted. + * @internal use @see{PersonHistory::__construct} instead */ public function addPersonHistory(PersonHistory $personHistory): void { $this->personHistories->add($personHistory); } + /** + * @internal use @see{MotiveHistory::__construct} instead + */ public function addMotiveHistory(MotiveHistory $motiveHistory): void { $this->motiveHistories->add($motiveHistory); diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AddCommentCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AddCommentCommandHandlerTest.php new file mode 100644 index 000000000..5ff18b436 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AddCommentCommandHandlerTest.php @@ -0,0 +1,52 @@ +buildCommand(); + + $ticket = new Ticket(); + $command = new AddCommentCommand(content: 'test'); + + $handler->handle($ticket, $command); + + self::assertCount(1, $ticket->getComments()); + self::assertEquals('test', $ticket->getComments()[0]->getContent()); + } + + private function buildCommand(): AddCommentCommandHandler + { + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::type(Comment::class))->shouldBeCalled(); + + return new AddCommentCommandHandler($entityManager->reveal()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/AddCommentControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/AddCommentControllerTest.php new file mode 100644 index 000000000..2826c360d --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/AddCommentControllerTest.php @@ -0,0 +1,104 @@ +validator = self::getContainer()->get(ValidatorInterface::class); + $this->serializer = self::getContainer()->get(SerializerInterface::class); + } + + public function testAddComment(): void + { + $controller = $this->buildController(willFlush: true); + + $ticket = new Ticket(); + $request = new Request(content: <<<'JSON' + {"content": "test"} + JSON); + + $response = $controller->__invoke($ticket, $request); + + self::assertEquals(201, $response->getStatusCode()); + } + + public function testAddCommentWithBlankContent(): void + { + $controller = $this->buildController(willFlush: false); + + $ticket = new Ticket(); + $request = new Request(content: <<<'JSON' + {"content": ""} + JSON); + + $response = $controller->__invoke($ticket, $request); + + self::assertEquals(422, $response->getStatusCode()); + + $request = new Request(content: <<<'JSON' + {"content": null} + JSON); + + $response = $controller->__invoke($ticket, $request); + + self::assertEquals(422, $response->getStatusCode()); + } + + private function buildController(bool $willFlush): AddCommentController + { + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + + if ($willFlush) { + $entityManager->persist(Argument::type(Comment::class))->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + } + + $commandHandler = new AddCommentCommandHandler($entityManager->reveal()); + + return new AddCommentController( + $security->reveal(), + $this->serializer, + $this->validator, + $commandHandler, + $entityManager->reveal(), + ); + } +} From 39d701feb2a60c212431c06042403b89da6adecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 18 Apr 2024 22:10:56 +0200 Subject: [PATCH 3/7] Serialize ticket's Comment --- .../ChillTicketBundle/src/Entity/Comment.php | 4 ++++ .../src/Resources/public/types.ts | 13 ++++++++++++- .../Serializer/Normalizer/TicketNormalizer.php | 10 ++++++++++ .../Normalizer/TicketNormalizerTest.php | 18 +++++++++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Comment.php b/src/Bundle/ChillTicketBundle/src/Entity/Comment.php index de12d5d47..8e11ff489 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Comment.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Comment.php @@ -17,9 +17,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\JoinColumn; +use Symfony\Component\Serializer\Annotation as Serializer; #[ORM\Entity()] #[ORM\Table(name: 'comment', schema: 'chill_ticket')] +#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_comment' => Comment::class])] class Comment implements TrackCreationInterface, TrackUpdateInterface { use TrackCreationTrait; @@ -28,10 +30,12 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface #[ORM\Id] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Serializer\Groups(['read'])] private ?int $id = null; public function __construct( #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] + #[Serializer\Groups(['read'])] private string $content, #[ORM\ManyToOne(targetEntity: Ticket::class, inversedBy: 'comments')] #[JoinColumn(nullable: false)] diff --git a/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts b/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts index b0c6a37df..2df53da99 100644 --- a/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts +++ b/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts @@ -36,10 +36,21 @@ interface MotiveHistory { createdAt: DateTime|null, } +interface Comment { + type: "ticket_comment", + id: number, + content: string, + createdBy: User|null, + createdAt: DateTime|null, + updatedBy: User|null, + updatedAt: DateTime|null, +} + interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {}; +interface AddCommentEvent extends TicketHistory<"add_comment", Comment> {}; interface SetMotiveEvent extends TicketHistory<"set_motive", MotiveHistory> {}; -type TicketHistoryLine = AddPersonEvent | SetMotiveEvent; +type TicketHistoryLine = AddPersonEvent | AddCommentEvent | SetMotiveEvent; export interface Ticket { type: "ticket_ticket" diff --git a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php index c69ac5a7f..c68ab34ef 100644 --- a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php +++ b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Serializer\Normalizer; +use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\PersonHistory; use Chill\TicketBundle\Entity\Ticket; @@ -69,6 +70,15 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte ], $ticket->getPersonHistories()->toArray(), ), + ...array_map( + fn (Comment $comment) => [ + 'event_type' => 'add_comment', + 'at' => $comment->getCreatedAt(), + 'by' => $comment->getCreatedBy(), + 'data' => $comment, + ], + $ticket->getComments()->toArray(), + ), ]; usort( diff --git a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php index e876341cd..447bebfbf 100644 --- a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php @@ -11,7 +11,9 @@ declare(strict_types=1); namespace Chill\TicketBundle\Tests\Serializer\Normalizer; +use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\Person; +use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\PersonHistory; @@ -93,11 +95,20 @@ class TicketNormalizerTest extends KernelTestCase // datetime $normalizer->normalize(Argument::type(\DateTimeImmutable::class), 'json', Argument::type('array')) ->will(function ($args) { return $args[0]->getTimestamp(); }); + // user + $normalizer->normalize(Argument::type(User::class), 'json', Argument::type('array')) + ->willReturn(['user']); + // motive $normalizer->normalize(Argument::type(Motive::class), 'json', Argument::type('array'))->willReturn(['type' => 'motive', 'id' => 0]); + // person history $normalizer->normalize(Argument::type(PersonHistory::class), 'json', Argument::type('array')) ->willReturn(['personHistory']); + // motive history $normalizer->normalize(Argument::type(MotiveHistory::class), 'json', Argument::type('array')) ->willReturn(['motiveHistory']); + $normalizer->normalize(Argument::type(Comment::class), 'json', Argument::type('array')) + ->willReturn(['comment']); + // null values $normalizer->normalize(null, 'json', Argument::type('array'))->willReturn(null); $ticketNormalizer = new TicketNormalizer(); @@ -109,6 +120,7 @@ class TicketNormalizerTest extends KernelTestCase public static function provideTickets(): iterable { yield [ + // this a nearly empty ticket new Ticket(), [ 'type' => 'ticket_ticket', @@ -122,10 +134,14 @@ class TicketNormalizerTest extends KernelTestCase ], ]; + // ticket with more features $ticket = new Ticket(); $ticket->setExternalRef('2134'); $personHistory = new PersonHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00')); $ticketHistory = new MotiveHistory(new Motive(), $ticket, new \DateTimeImmutable('2024-04-01T12:02:00')); + $comment = new Comment('blabla test', $ticket); + $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00')); + $comment->setCreatedBy(new User()); yield [ $ticket, @@ -137,7 +153,7 @@ class TicketNormalizerTest extends KernelTestCase 'currentAddressees' => [], 'currentInputs' => [], 'currentMotive' => ['type' => 'motive', 'id' => 0], - 'history' => [['event_type' => 'add_person'], ['event_type' => 'set_motive']], + 'history' => [['event_type' => 'add_person'], ['event_type' => 'set_motive'], ['event_type' => 'add_comment']], ], ]; } From e2deb55fdbee985636d5862c7d98cab4d39afc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 19 Apr 2024 15:34:16 +0200 Subject: [PATCH 4/7] Create api endpoint for listing user-group --- .../Controller/UserGroupApiController.php | 16 ++++++++++++++++ .../DependencyInjection/ChillMainExtension.php | 17 +++++++++++++++++ src/Bundle/ChillMainBundle/Entity/UserGroup.php | 4 ++++ src/Bundle/ChillMainBundle/chill.api.specs.yaml | 16 ++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Controller/UserGroupApiController.php diff --git a/src/Bundle/ChillMainBundle/Controller/UserGroupApiController.php b/src/Bundle/ChillMainBundle/Controller/UserGroupApiController.php new file mode 100644 index 000000000..602b84ec5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/UserGroupApiController.php @@ -0,0 +1,16 @@ + UserGroup::class, + 'controller' => UserGroupApiController::class, + 'name' => 'user-group', + 'base_path' => '/api/1.0/main/user-group', + 'base_role' => 'ROLE_USER', + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + ], + ], ], ]); } diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 3bd9870a6..d6e343d00 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -14,17 +14,21 @@ namespace Chill\MainBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; #[ORM\Entity] #[ORM\Table(name: 'chill_main_user_group')] +#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['user_group' => UserGroup::class])] class UserGroup { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] + #[Serializer\Groups(['read'])] private ?int $id = null; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + #[Serializer\Groups(['read'])] private array $label = []; /** diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index f37ee723d..fec36312d 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -908,3 +908,19 @@ paths: $ref: '#/components/schemas/NewsItem' 403: description: "Unauthorized" + /1.0/main/user-group.json: + get: + tags: + - user-group + summary: Return a list of users-groups + responses: + 200: + description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NewsItem' + 403: + description: "Unauthorized" From 46fb1c04b57cd6fd4224e389e8d6720e67027ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 22 Apr 2024 12:01:28 +0200 Subject: [PATCH 5/7] Add color and exclusion fields to UserGroup This commit introduces new fields to the UserGroup entity, specifically background color, foreground color, and an exclusion key. These have been implemented both in the PHP entity and TypeScript interface definitions. Additionally, a Doctrine migration has been created to reflect these changes on the database side. --- .../ChillMainBundle/Entity/UserGroup.php | 61 +++++++++++++++++++ .../ChillMainBundle/Resources/public/types.ts | 9 +++ .../migrations/Version20240422091752.php | 41 +++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240422091752.php diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index d6e343d00..470a21a70 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -38,6 +38,24 @@ class UserGroup #[ORM\JoinTable(name: 'chill_main_user_group_user')] private Collection $users; + #[ORM\Column(type: 'text', nullable: false, options: ['default' => '#ffffffff'])] + #[Serializer\Groups(['read'])] + private string $backgroundColor = '#ffffffff'; + + #[ORM\Column(type: 'text', nullable: false, options: ['default' => '#000000ff'])] + #[Serializer\Groups(['read'])] + private string $foregroundColor = '#000000ff'; + + /** + * Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship + * will exclude others. + * + * An empty string means "no exclusion" + */ + #[ORM\Column(type: 'text', nullable: false, options: ['default' => ''])] + #[Serializer\Groups(['read'])] + private string $excludeKey = ''; + public function __construct() { $this->users = new ArrayCollection(); @@ -75,4 +93,47 @@ class UserGroup { return $this->users; } + + public function getForegroundColor(): string + { + return $this->foregroundColor; + } + + public function getExcludeKey(): string + { + return $this->excludeKey; + } + + public function getBackgroundColor(): string + { + return $this->backgroundColor; + } + + public function setForegroundColor(string $foregroundColor): self + { + $this->foregroundColor = $foregroundColor; + + return $this; + } + + public function setBackgroundColor(string $backgroundColor): self + { + $this->backgroundColor = $backgroundColor; + + return $this; + } + + public function setExcludeKey(string $excludeKey): self + { + $this->excludeKey = $excludeKey; + + return $this; + } + + public function setLabel(array $label): self + { + $this->label = $label; + + return $this; + } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 2e33b8248..840a0e939 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -42,6 +42,15 @@ export interface User { // todo: mainCenter; mainJob; etc.. } +export interface UserGroup { + type: "chill_main_user_group", + id: number, + label: TranslatableString, + backgroundColor: string, + foregroundColor: string, + excludeKey: string, +} + export interface UserAssociatedInterface { type: "user"; id: number; diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240422091752.php b/src/Bundle/ChillMainBundle/migrations/Version20240422091752.php new file mode 100644 index 000000000..960b3cc79 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240422091752.php @@ -0,0 +1,41 @@ +addSql('ALTER TABLE chill_main_user_group ADD backgroundColor TEXT DEFAULT \'#ffffffff\' NOT NULL'); + $this->addSql('ALTER TABLE chill_main_user_group ADD foregroundColor TEXT DEFAULT \'#000000ff\' NOT NULL'); + $this->addSql('ALTER TABLE chill_main_user_group ADD excludeKey TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('ALTER INDEX idx_1e07f044d2112630 RENAME TO IDX_738BC82BD2112630'); + $this->addSql('ALTER INDEX idx_1e07f044a76ed395 RENAME TO IDX_738BC82BA76ED395'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_user_group DROP backgroundColor'); + $this->addSql('ALTER TABLE chill_main_user_group DROP foregroundColor'); + $this->addSql('ALTER TABLE chill_main_user_group DROP excludeKey'); + $this->addSql('ALTER INDEX idx_738bc82bd2112630 RENAME TO idx_1e07f044d2112630'); + $this->addSql('ALTER INDEX idx_738bc82ba76ed395 RENAME TO idx_1e07f044a76ed395'); + } +} From 0bc6e62d4d6d29be4c593990cc956edfb4be226e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 22 Apr 2024 12:01:49 +0200 Subject: [PATCH 6/7] Add fixtures for UserGroup --- .../DataFixtures/ORM/LoadUserGroup.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php new file mode 100644 index 000000000..614a83515 --- /dev/null +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php @@ -0,0 +1,68 @@ +getRepository(User::class)->findOneBy(['username' => 'center a_social']); + $centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']); + $multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']); + $administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']); + $administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']); + + $level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level'); + $level1->addUser($centerASocial)->addUser($centerBSocial); + $manager->persist($level1); + + $level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level'); + $level2->addUser($multiCenter); + $manager->persist($level2); + + $level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level'); + $level3->addUser($multiCenter); + $manager->persist($level3); + + $tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', ''); + $tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial); + $manager->persist($tss); + $admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', ''); + $admins->addUser($administrativeA)->addUser($administrativeB); + $manager->persist($admins); + + $manager->flush(); + } + + private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup + { + $userGroup = new UserGroup(); + + return $userGroup + ->setLabel(['fr' => $title]) + ->setBackgroundColor($backgroundColor) + ->setForegroundColor($foregroundColor) + ->setExcludeKey($excludeKey) + ; + } +} From 9f355032a818dfc2659451fa0652801f393fb90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 22 Apr 2024 12:41:43 +0200 Subject: [PATCH 7/7] Create a "do not exclude" validation constraint for user groups --- .../Validator/UserGroupDoNotExcludeTest.php | 91 +++++++++++++++++++ .../Constraint/UserGroupDoNotExclude.php | 31 +++++++ .../Validator/UserGroupDoNotExclude.php | 69 ++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Tests/Validation/Validator/UserGroupDoNotExcludeTest.php create mode 100644 src/Bundle/ChillMainBundle/Validation/Constraint/UserGroupDoNotExclude.php create mode 100644 src/Bundle/ChillMainBundle/Validation/Validator/UserGroupDoNotExclude.php diff --git a/src/Bundle/ChillMainBundle/Tests/Validation/Validator/UserGroupDoNotExcludeTest.php b/src/Bundle/ChillMainBundle/Tests/Validation/Validator/UserGroupDoNotExcludeTest.php new file mode 100644 index 000000000..d6d1c9887 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Validation/Validator/UserGroupDoNotExcludeTest.php @@ -0,0 +1,91 @@ +validator->validate([], new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude()); + + $this->assertNoViolation(); + } + + public function testMixedUserGroupAndUsersIsValid(): void + { + $this->validator->validate( + [new User(), new UserGroup()], + new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude() + ); + + $this->assertNoViolation(); + } + + public function testDifferentExcludeKeysIsValid(): void + { + $this->validator->validate( + [(new UserGroup())->setExcludeKey('A'), (new UserGroup())->setExcludeKey('B')], + new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude() + ); + + $this->assertNoViolation(); + } + + public function testMultipleGroupsWithEmptyExcludeKeyIsValid(): void + { + $this->validator->validate( + [(new UserGroup())->setExcludeKey(''), (new UserGroup())->setExcludeKey('')], + new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude() + ); + + $this->assertNoViolation(); + } + + public function testSameExclusionKeyWillRaiseError(): void + { + $this->validator->validate( + [ + (new UserGroup())->setExcludeKey('A')->setLabel(['fr' => 'Group 1']), + (new UserGroup())->setExcludeKey('A')->setLabel(['fr' => 'Group 2']), + ], + new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude() + ); + + $this->buildViolation('The groups {{ excluded_groups }} do exclude themselves. Please choose one between them') + ->setParameter('excluded_groups', 'Group 1, Group 2') + ->setCode('e16c8226-0090-11ef-8560-f7239594db09') + ->assertRaised(); + } +} diff --git a/src/Bundle/ChillMainBundle/Validation/Constraint/UserGroupDoNotExclude.php b/src/Bundle/ChillMainBundle/Validation/Constraint/UserGroupDoNotExclude.php new file mode 100644 index 000000000..5ec688e5b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Validation/Constraint/UserGroupDoNotExclude.php @@ -0,0 +1,31 @@ +getExcludeKey()][] = $gr; + } + } + + foreach ($groups as $excludeKey => $groupByKey) { + if ('' === $excludeKey) { + continue; + } + + if (1 < count($groupByKey)) { + $excludedGroups = implode( + ', ', + array_map( + fn (UserGroup $group) => $this->translatableStringHelper->localize($group->getLabel()), + $groupByKey + ) + ); + + $this->context + ->buildViolation($constraint->message) + ->setCode($constraint->code) + ->setParameters(['excluded_groups' => $excludedGroups]) + ->addViolation(); + } + } + } +}