From b4fa478177671258b31e5f97fc4fdc14880fd178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 11:17:50 +0200 Subject: [PATCH 01/19] Add UserGroup to chill (import from branch ticket-app-master) Import the UserGrou feature from ticket-app-master branch. This includes: - import all the entities and migrations, modification of typescript types, templating, and so on; - apply some verification and formatting rules, like: - reformat file on chill.api.specs.yaml (MainBundle) - reformat file on types.ts (Main Bundle) Migrations kept the same filename. --- .../Controller/UserGroupApiController.php | 16 + .../DataFixtures/ORM/LoadUserGroup.php | 68 ++++ .../ChillMainExtension.php | 17 + .../ChillMainBundle/Entity/UserGroup.php | 151 ++++++++ .../ChillMainBundle/Resources/public/types.ts | 229 ++++++------ .../views/Entity/user_group.html.twig | 1 + .../Templating/Entity/UserGroupRender.php | 38 ++ .../Entity/UserGroupRenderInterface.php | 14 + .../Constraint/UserGroupDoNotExclude.php | 31 ++ .../Validator/UserGroupDoNotExclude.php | 69 ++++ .../ChillMainBundle/chill.api.specs.yaml | 352 ++++++++++-------- .../config/services/templating.yaml | 4 + .../config/services/validator.yaml | 3 + .../migrations/Version20240416145021.php | 41 ++ .../migrations/Version20240422091752.php | 41 ++ .../Resources/public/chill/scss/badge.scss | 7 + 16 files changed, 823 insertions(+), 259 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/UserGroupApiController.php create mode 100644 src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php create mode 100644 src/Bundle/ChillMainBundle/Entity/UserGroup.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Entity/user_group.html.twig create mode 100644 src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php create mode 100644 src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRenderInterface.php create mode 100644 src/Bundle/ChillMainBundle/Validation/Constraint/UserGroupDoNotExclude.php create mode 100644 src/Bundle/ChillMainBundle/Validation/Validator/UserGroupDoNotExclude.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240416145021.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240422091752.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 @@ +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) + ; + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 9d788c64e..97df404af 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -24,6 +24,7 @@ use Chill\MainBundle\Controller\LocationTypeController; use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\UserController; +use Chill\MainBundle\Controller\UserGroupApiController; use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobController; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; @@ -59,6 +60,7 @@ use Chill\MainBundle\Entity\LocationType; use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Form\CenterType; use Chill\MainBundle\Form\CivilityType; @@ -803,6 +805,21 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => 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 new file mode 100644 index 000000000..03da74326 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -0,0 +1,151 @@ + 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 = []; + + /** + * @var \Doctrine\Common\Collections\Collection + */ + #[ORM\ManyToMany(targetEntity: User::class)] + #[ORM\JoinTable(name: 'chill_main_user_group_user')] + private Collection $users; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])] + #[Serializer\Groups(['read'])] + private string $backgroundColor = '#ffffffff'; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::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: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] + #[Serializer\Groups(['read'])] + private string $excludeKey = ''; + + public function __construct() + { + $this->users = new ArrayCollection(); + } + + public function addUser(User $user): self + { + if (!$this->users->contains($user)) { + $this->users[] = $user; + } + + return $this; + } + + public function removeUser(User $user): self + { + if ($this->users->contains($user)) { + $this->users->removeElement($user); + } + + return $this; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): array + { + return $this->label; + } + + public function getUsers(): Collection + { + 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; + } + + /** + * Checks if the current object is an instance of the UserGroup class. + * + * In use in twig template, to discriminate when there an object can be polymorphic. + * + * @return bool returns true if the current object is an instance of UserGroup, false otherwise + */ + public function isUserGroup(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 304ebd8e4..8d2ea2ded 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -1,164 +1,175 @@ export interface DateTime { - datetime: string; - datetime8601: string + datetime: string; + datetime8601: string; } export interface Civility { - id: number; - // TODO + id: number; + // TODO } export interface Job { - id: number; - type: "user_job"; - label: { - "fr": string; // could have other key. How to do that in ts ? - } + id: number; + type: "user_job"; + label: { + fr: string; // could have other key. How to do that in ts ? + }; } export interface Center { - id: number; - type: "center"; - name: string; + id: number; + type: "center"; + name: string; } export interface Scope { - id: number; - type: "scope"; - name: { - "fr": string - } + id: number; + type: "scope"; + name: { + fr: string; + }; } export interface User { - type: "user"; - id: number; - username: string; - text: string; - text_without_absence: string; - email: string; - user_job: Job; - label: string; - // todo: mainCenter; mainJob; etc.. + type: "user"; + id: number; + username: string; + text: string; + text_without_absence: string; + email: string; + user_job: Job; + label: string; + // todo: mainCenter; mainJob; etc.. } +export interface UserGroup { + type: "chill_main_user_group" | "user_group"; + id: number; + label: TranslatableString; + backgroundColor: string; + foregroundColor: string; + excludeKey: string; +} + +export type UserGroupOrUser = User | UserGroup; + export interface UserAssociatedInterface { - type: "user"; - id: number; -}; - -export type TranslatableString = { - fr?: string; - nl?: string; + type: "user"; + id: number; } +export type TranslatableString = { + fr?: string; + nl?: string; +}; + export interface Postcode { - id: number; - name: string; - code: string; - center: Point; + id: number; + name: string; + code: string; + center: Point; } export type Point = { - type: "Point"; - coordinates: [lat: number, lon: number]; -} + type: "Point"; + coordinates: [lat: number, lon: number]; +}; export interface Country { - id: number; - name: TranslatableString; - code: string; + id: number; + name: TranslatableString; + code: string; } -export type AddressRefStatus = 'match'|'to_review'|'reviewed'; +export type AddressRefStatus = "match" | "to_review" | "reviewed"; export interface Address { - type: "address"; - address_id: number; - text: string; - street: string; - streetNumber: string; - postcode: Postcode; - country: Country; - floor: string | null; - corridor: string | null; - steps: string | null; - flat: string | null; - buildingName: string | null; - distribution: string | null; - extra: string | null; - confidential: boolean; - lines: string[]; - addressReference: AddressReference | null; - validFrom: DateTime; - validTo: DateTime | null; - point: Point | null; - refStatus: AddressRefStatus; - isNoAddress: boolean; + type: "address"; + address_id: number; + text: string; + street: string; + streetNumber: string; + postcode: Postcode; + country: Country; + floor: string | null; + corridor: string | null; + steps: string | null; + flat: string | null; + buildingName: string | null; + distribution: string | null; + extra: string | null; + confidential: boolean; + lines: string[]; + addressReference: AddressReference | null; + validFrom: DateTime; + validTo: DateTime | null; + point: Point | null; + refStatus: AddressRefStatus; + isNoAddress: boolean; } export interface AddressWithPoint extends Address { - point: Point + point: Point; } export interface AddressReference { - id: number; - createdAt: DateTime | null; - deletedAt: DateTime | null; - municipalityCode: string; - point: Point; - postcode: Postcode; - refId: string; - source: string; - street: string; - streetNumber: string; - updatedAt: DateTime | null; + id: number; + createdAt: DateTime | null; + deletedAt: DateTime | null; + municipalityCode: string; + point: Point; + postcode: Postcode; + refId: string; + source: string; + street: string; + streetNumber: string; + updatedAt: DateTime | null; } export interface SimpleGeographicalUnit { - id: number; - layerId: number; - unitName: string; - unitRefId: string; + id: number; + layerId: number; + unitName: string; + unitRefId: string; } export interface GeographicalUnitLayer { - id: number; - name: TranslatableString; - refId: string; + id: number; + name: TranslatableString; + refId: string; } export interface Location { - type: "location"; - id: number; - active: boolean; - address: Address | null; - availableForUsers: boolean; - createdAt: DateTime | null; - createdBy: User | null; - updatedAt: DateTime | null; - updatedBy: User | null; - email: string | null - name: string; - phonenumber1: string | null; - phonenumber2: string | null; - locationType: LocationType; + type: "location"; + id: number; + active: boolean; + address: Address | null; + availableForUsers: boolean; + createdAt: DateTime | null; + createdBy: User | null; + updatedAt: DateTime | null; + updatedBy: User | null; + email: string | null; + name: string; + phonenumber1: string | null; + phonenumber2: string | null; + locationType: LocationType; } export interface LocationAssociated { - type: "location"; - id: number; + type: "location"; + id: number; } export interface LocationType { - type: "location-type"; - id: number; - active: boolean; - addressRequired: "optional" | "required"; - availableForUsers: boolean; - editableByUsers: boolean; - contactData: "optional" | "required"; - title: TranslatableString; + type: "location-type"; + id: number; + active: boolean; + addressRequired: "optional" | "required"; + availableForUsers: boolean; + editableByUsers: boolean; + contactData: "optional" | "required"; + title: TranslatableString; } export interface NewsItemType { diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user_group.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user_group.html.twig new file mode 100644 index 000000000..cb6c3be40 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user_group.html.twig @@ -0,0 +1 @@ +{{ user_group.label|localize_translatable_string }} diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php new file mode 100644 index 000000000..b63fca8cf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php @@ -0,0 +1,38 @@ +environment->render('@ChillMain/Entity/user_group.html.twig', ['user_group' => $entity]); + } + + public function renderString($entity, array $options): string + { + /* @var $entity UserGroup */ + return $this->translatableStringHelper->localize($entity->getLabel()); + } + + public function supports(object $entity, array $options): bool + { + return $entity instanceof UserGroup; + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRenderInterface.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRenderInterface.php new file mode 100644 index 000000000..7ebf70028 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRenderInterface.php @@ -0,0 +1,14 @@ +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(); + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 1abdfc72e..b31669043 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -5,8 +5,8 @@ info: title: "Chill api" description: "Api documentation for chill. Currently, work in progress" servers: - - url: "/api" - description: "Your current dev server" + - url: "/api" + description: "Your current dev server" components: schemas: @@ -29,6 +29,42 @@ components: type: string text: type: string + UserById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user + UserGroup: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user_group + label: + type: object + additionalProperties: true + backgroundColor: + type: string + foregroundColor: + type: string + exclusionKey: + type: string + UserGroupById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user_group Center: type: object properties: @@ -181,25 +217,25 @@ paths: The results are ordered by relevance, from the most to the lowest relevant. parameters: - - name: q - in: query - required: true - description: the pattern to search - schema: - type: string - - name: type[] - in: query - required: true - description: the type entities amongst the search is performed - schema: - type: array - items: - type: string - enum: - - person - - thirdparty - - user - - household + - name: q + in: query + required: true + description: the pattern to search + schema: + type: string + - name: type[] + in: query + required: true + description: the type entities amongst the search is performed + schema: + type: array + items: + type: string + enum: + - person + - thirdparty + - user + - household responses: 200: description: "OK" @@ -236,7 +272,7 @@ paths: minItems: 2 maxItems: 2 postcode: - $ref: "#/components/schemas/PostalCode" + $ref: '#/components/schemas/PostalCode' steps: type: string street: @@ -260,14 +296,14 @@ paths: - address summary: Return an address by id parameters: - - name: id - in: path - required: true - description: The address id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The address id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -284,14 +320,14 @@ paths: - address summary: patch an address parameters: - - name: id - in: path - required: true - description: The address id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The address id + schema: + type: integer + format: integer + minimum: 1 requestBody: required: true content: @@ -349,14 +385,14 @@ paths: - address summary: Duplicate an existing address parameters: - - name: id - in: path - required: true - description: The address id that will be duplicated - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The address id that will be duplicated + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -375,12 +411,12 @@ paths: - address summary: Return a list of all reference addresses parameters: - - in: query - name: postal_code - required: false - schema: - type: integer - description: The id of a postal code to filter the reference addresses + - in: query + name: postal_code + required: false + schema: + type: integer + description: The id of a postal code to filter the reference addresses responses: 200: description: "ok" @@ -390,14 +426,14 @@ paths: - address summary: Return a reference address by id parameters: - - name: id - in: path - required: true - description: The reference address id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The reference address id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -417,20 +453,20 @@ paths: - search summary: Return a reference address by id parameters: - - name: id - in: path - required: true - description: The reference address id - schema: - type: integer - format: integer - minimum: 1 - - name: q - in: query - required: true - description: The search pattern - schema: - type: string + - name: id + in: path + required: true + description: The reference address id + schema: + type: integer + format: integer + minimum: 1 + - name: q + in: query + required: true + description: The search pattern + schema: + type: string responses: 200: description: "ok" @@ -450,12 +486,12 @@ paths: - address summary: Return a list of all postal-code parameters: - - in: query - name: country - required: false - schema: - type: integer - description: The id of a country to filter the postal code + - in: query + name: country + required: false + schema: + type: integer + description: The id of a country to filter the postal code responses: 200: description: "ok" @@ -494,14 +530,14 @@ paths: - address summary: Return a postal code by id parameters: - - name: id - in: path - required: true - description: The postal code id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The postal code id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -521,18 +557,18 @@ paths: - search summary: Search a postal code parameters: - - name: q - in: query - required: true - description: The search pattern - schema: - type: string - - name: country - in: query - required: false - description: The country id - schema: - type: integer + - name: q + in: query + required: true + description: The search pattern + schema: + type: string + - name: country + in: query + required: false + description: The country id + schema: + type: integer responses: 200: description: "ok" @@ -559,14 +595,14 @@ paths: - address summary: Return a country by id parameters: - - name: id - in: path - required: true - description: The country id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The country id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -609,14 +645,14 @@ paths: - user summary: Return a user by id parameters: - - name: id - in: path - required: true - description: The user id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The user id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -646,14 +682,14 @@ paths: - scope summary: return a list of scopes parameters: - - name: id - in: path - required: true - description: The scope id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The scope id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -721,14 +757,14 @@ paths: - location summary: Return the given location parameters: - - name: id - in: path - required: true - description: The location id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The location id + schema: + type: integer + format: integer + minimum: 1 responses: 200: description: "ok" @@ -788,14 +824,14 @@ paths: - notification summary: mark a notification as read parameters: - - name: id - in: path - required: true - description: The notification id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The notification id + schema: + type: integer + format: integer + minimum: 1 responses: 202: description: "accepted" @@ -807,14 +843,14 @@ paths: - notification summary: mark a notification as unread parameters: - - name: id - in: path - required: true - description: The notification id - schema: - type: integer - format: integer - minimum: 1 + - name: id + in: path + required: true + description: The notification id + schema: + type: integer + format: integer + minimum: 1 responses: 202: description: "accepted" @@ -846,7 +882,7 @@ paths: type: array items: type: integer - example: [1, 2, 3] # Example array of IDs + example: [ 1, 2, 3 ] # Example array of IDs responses: "202": description: Notifications marked as unread successfully @@ -934,6 +970,22 @@ paths: schema: type: array items: - $ref: "#/components/schemas/NewsItem" + $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/UserGroup' 403: description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index 281d4ad23..04117bf16 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -56,6 +56,10 @@ services: Chill\MainBundle\Templating\Entity\UserRender: ~ + Chill\MainBundle\Templating\Entity\UserGroupRender: ~ + Chill\MainBundle\Templating\Entity\UserGroupRenderInterface: + alias: Chill\MainBundle\Templating\Entity\UserGroupRender + Chill\MainBundle\Templating\Listing\: resource: './../../Templating/Listing' diff --git a/src/Bundle/ChillMainBundle/config/services/validator.yaml b/src/Bundle/ChillMainBundle/config/services/validator.yaml index b3b60b9d6..32b8903cc 100644 --- a/src/Bundle/ChillMainBundle/config/services/validator.yaml +++ b/src/Bundle/ChillMainBundle/config/services/validator.yaml @@ -3,6 +3,9 @@ services: autowire: true autoconfigure: true + Chill\MainBundle\Validation\: + resource: '../../Validation' + chill_main.validator_user_circle_consistency: class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator arguments: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240416145021.php b/src/Bundle/ChillMainBundle/migrations/Version20240416145021.php new file mode 100644 index 000000000..f2f8d37dd --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240416145021.php @@ -0,0 +1,41 @@ +addSql('CREATE SEQUENCE chill_main_user_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_user_group (id INT NOT NULL, label JSON DEFAULT \'[]\' NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE chill_main_user_group_user (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))'); + $this->addSql('CREATE INDEX IDX_1E07F044D2112630 ON chill_main_user_group_user (usergroup_id)'); + $this->addSql('CREATE INDEX IDX_1E07F044A76ED395 ON chill_main_user_group_user (user_id)'); + $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_user_group_id_seq'); + $this->addSql('DROP TABLE chill_main_user_group_user'); + $this->addSql('DROP TABLE chill_main_user_group'); + } +} 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'); + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss index 072f13949..d1faf5614 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss @@ -3,8 +3,10 @@ */ span.badge-user, +span.badge-user-group, span.badge-person, span.badge-thirdparty { + margin: 0.2rem 0.1rem; display: inline-block; padding: 0 0.5em !important; background-color: $white; @@ -18,6 +20,11 @@ span.badge-thirdparty { } } +span.badge-user-group { + font-weight: 600; + border-width: 0px; +} + span.badge-user { border-bottom-width: 1px; &.system { From 9e69c972508980ad9092f7cbf65979cfb3d7726c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 14:17:19 +0200 Subject: [PATCH 02/19] Add search functionality for user groups Implemented `SearchUserGroupApiProvider` to handle user group search requests. Added `UserGroupRepository` and its interface to support search queries. Updated API specs to include user group as a searchable type. --- .../Repository/UserGroupRepository.php | 68 +++++++++++++++++++ .../UserGroupRepositoryInterface.php | 27 ++++++++ .../Entity/SearchUserGroupApiProvider.php | 59 ++++++++++++++++ .../Repository/UserGroupRepositoryTest.php | 50 ++++++++++++++ .../ChillMainBundle/chill.api.specs.yaml | 1 + 5 files changed, 205 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php create mode 100644 src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php create mode 100644 src/Bundle/ChillMainBundle/Search/Entity/SearchUserGroupApiProvider.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Repository/UserGroupRepositoryTest.php diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php new file mode 100644 index 000000000..aa2d6f5f5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php @@ -0,0 +1,68 @@ +repository = $em->getRepository(UserGroup::class); + } + + public function find($id): ?UserGroup + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?UserGroup + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return UserGroup::class; + } + + public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery + { + $query = new SearchApiQuery(); + $query + ->setSelectKey($selectKey) + ->setSelectJsonbMetadata("jsonb_build_object('id', ug.id)") + ->setSelectPertinence('3 + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) + CASE WHEN (EXISTS(SELECT 1 FROM unnest(string_to_array(label->>?, \' \')) AS t WHERE LOWER(t) LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')) THEN 100 ELSE 0 END', [$pattern, $lang, $lang, $pattern]) + ->setFromClause('chill_main_user_group AS ug') + ->setWhereClauses(' + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) > 0.15 + OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\' + ', [$pattern, $lang, $pattern, $lang]); + + return $query; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php new file mode 100644 index 000000000..9512e7b25 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php @@ -0,0 +1,27 @@ + + */ +interface UserGroupRepositoryInterface extends ObjectRepository +{ + /** + * Provide a SearchApiQuery for searching amongst user groups. + */ + public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery; +} diff --git a/src/Bundle/ChillMainBundle/Search/Entity/SearchUserGroupApiProvider.php b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserGroupApiProvider.php new file mode 100644 index 000000000..acdd99976 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserGroupApiProvider.php @@ -0,0 +1,59 @@ +locale = $locale; + } + + public function getLocale(): string + { + return $this->locale; + } + + public function getResult(string $key, array $metadata, float $pertinence) + { + return $this->userGroupRepository->find($metadata['id']); + } + + public function prepare(array $metadatas): void {} + + public function provideQuery(string $pattern, array $parameters): SearchApiQuery + { + return $this->userGroupRepository->provideSearchApiQuery($pattern, $this->getLocale(), 'user-group'); + } + + public function supportsResult(string $key, array $metadatas): bool + { + return 'user-group' === $key; + } + + public function supportsTypes(string $pattern, array $types, array $parameters): bool + { + return in_array('user-group', $types, true); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/UserGroupRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/UserGroupRepositoryTest.php new file mode 100644 index 000000000..1ea243541 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Repository/UserGroupRepositoryTest.php @@ -0,0 +1,50 @@ +entityManager = static::getContainer()->get(EntityManagerInterface::class); + + } + + public function testProvideSearchApiQuery(): void + { + $repository = new UserGroupRepository($this->entityManager); + + $apiQuery = $repository->provideSearchApiQuery('trav', 'fr'); + + // test that the query does works + $sql = $apiQuery->buildQuery(); + $params = $apiQuery->buildParameters(); + + $result = $this->entityManager->getConnection()->executeQuery($sql, $params); + $results = $result->fetchAllAssociative(); + + self::assertIsArray($results); + + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index b31669043..ea1ae10bd 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -236,6 +236,7 @@ paths: - thirdparty - user - household + - user-group responses: 200: description: "OK" From 82cd77678b821cc48fd22d4be691479f3f6cad4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 15:10:34 +0200 Subject: [PATCH 03/19] Create a PickUserGroupOrUserDynamicType - add necessary vue component to render usergroup within the component AddPersons; - add necessary normalization and denormalization process for matching the selected usergroup with entities in database --- .../ChillMainBundle/Entity/UserGroup.php | 7 -- .../EntityToJsonTransformer.php | 2 + .../Type/PickUserGroupOrUserDynamicType.php | 68 +++++++++++++++++++ .../ChillMainBundle/Resources/public/types.ts | 8 ++- .../_components/Entity/UserGroupRenderBox.vue | 26 +++++++ .../Normalizer/UserGroupDenormalizer.php | 37 ++++++++++ .../Normalizer/UserGroupNormalizer.php | 41 +++++++++++ .../Normalizer/UserGroupDenormalizerTest.php | 62 +++++++++++++++++ .../Normalizer/UserGroupNormalizerTest.php | 56 +++++++++++++++ .../Resources/public/chill/scss/badge.scss | 4 ++ .../AddPersons/PersonSuggestion.vue | 7 ++ .../_components/AddPersons/TypeUserGroup.vue | 30 ++++++++ 12 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserGroupRenderBox.vue create mode 100644 src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupDenormalizer.php create mode 100644 src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupNormalizer.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupDenormalizerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupNormalizerTest.php create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 03da74326..588013b65 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -14,21 +14,17 @@ 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 = []; /** @@ -39,11 +35,9 @@ class UserGroup private Collection $users; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])] - #[Serializer\Groups(['read'])] private string $backgroundColor = '#ffffffff'; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])] - #[Serializer\Groups(['read'])] private string $foregroundColor = '#000000ff'; /** @@ -53,7 +47,6 @@ class UserGroup * An empty string means "no exclusion" */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] - #[Serializer\Groups(['read'])] private string $excludeKey = ''; public function __construct() diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php index d193ea2ef..737c4683f 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type\DataTransformer; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\PersonBundle\Entity\Person; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Symfony\Component\Form\DataTransformerInterface; @@ -74,6 +75,7 @@ class EntityToJsonTransformer implements DataTransformerInterface 'user' => User::class, 'person' => Person::class, 'thirdparty' => ThirdParty::class, + 'user_group' => UserGroup::class, default => throw new \UnexpectedValueException('This type is not supported'), }; diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php b/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php new file mode 100644 index 000000000..00d783a97 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php @@ -0,0 +1,68 @@ +addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group')); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['multiple'] = $options['multiple']; + $view->vars['types'] = ['user-group', 'user']; + $view->vars['uniqid'] = uniqid('pick_usergroup_dyn'); + $view->vars['suggested'] = []; + $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0'; + $view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0'; + + foreach ($options['suggested'] as $userGroup) { + $view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']); + } + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('multiple', false) + ->setAllowedTypes('multiple', ['bool']) + ->setDefault('compound', false) + ->setDefault('suggested', []) + // if set to true, only the id will be set inside the content. The denormalization will not work. + ->setDefault('as_id', false) + ->setAllowedTypes('as_id', ['bool']) + ->setDefault('submit_on_adding_new_entity', false) + ->setAllowedTypes('submit_on_adding_new_entity', ['bool']); + } + + public function getBlockPrefix() + { + return 'pick_entity_dynamic'; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 8d2ea2ded..9df338549 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -30,6 +30,11 @@ export interface Scope { }; } +export interface ResultItem { + result: T; + relevance: number; +} + export interface User { type: "user"; id: number; @@ -43,12 +48,13 @@ export interface User { } export interface UserGroup { - type: "chill_main_user_group" | "user_group"; + type: "user_group"; id: number; label: TranslatableString; backgroundColor: string; foregroundColor: string; excludeKey: string; + text: string; } export type UserGroupOrUser = User | UserGroup; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserGroupRenderBox.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserGroupRenderBox.vue new file mode 100644 index 000000000..d25100776 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserGroupRenderBox.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupDenormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupDenormalizer.php new file mode 100644 index 000000000..0960ea218 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupDenormalizer.php @@ -0,0 +1,37 @@ +userGroupRepository->find($data['id']); + } + + public function supportsDenormalization($data, string $type, ?string $format = null): bool + { + return UserGroup::class === $type + && 'json' === $format + && is_array($data) + && array_key_exists('id', $data) + && 'user_group' === ($data['type'] ?? false) + && 2 === count(array_keys($data)) + ; + } +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupNormalizer.php new file mode 100644 index 000000000..8738167eb --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserGroupNormalizer.php @@ -0,0 +1,41 @@ + 'user_group', + 'id' => $object->getId(), + 'label' => $object->getLabel(), + 'backgroundColor' => $object->getBackgroundColor(), + 'foregroundColor' => $object->getForegroundColor(), + 'excludeKey' => $object->getExcludeKey(), + 'text' => $this->userGroupRender->renderString($object, []), + ]; + } + + public function supportsNormalization($data, ?string $format = null) + { + return $data instanceof UserGroup; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupDenormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupDenormalizerTest.php new file mode 100644 index 000000000..95a633519 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupDenormalizerTest.php @@ -0,0 +1,62 @@ +createMock(UserGroupRepositoryInterface::class); + $denormalizer = new UserGroupDenormalizer($repository); + + $actual = $denormalizer->supportsDenormalization($data, $type, 'json'); + + self::assertSame($expected, $actual); + } + + public static function provideSupportsDenormalization(): iterable + { + yield [['type' => 'user_group', 'id' => 10], UserGroup::class, true]; + yield [['type' => 'person', 'id' => 10], UserGroup::class, false]; + yield [['type' => 'user_group', 'id' => 10], \stdClass::class, false]; + } + + public function testDenormalize(): void + { + $repository = $this->createMock(UserGroupRepositoryInterface::class); + $repository->expects($this->once()) + ->method('find') + ->with(10) + ->willReturn($userGroup = new UserGroup()); + + $denormalizer = new UserGroupDenormalizer($repository); + + $actual = $denormalizer->denormalize(['type' => 'user_group', 'id' => 10], UserGroup::class, 'json'); + + self::assertSame($userGroup, $actual); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupNormalizerTest.php new file mode 100644 index 000000000..eb85c99a7 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserGroupNormalizerTest.php @@ -0,0 +1,56 @@ +setLabel(['fr' => 'test']) + ->setExcludeKey('top') + ->setForegroundColor('#123456') + ->setBackgroundColor('#456789'); + + $entityRender = $this->createMock(UserGroupRenderInterface::class); + $entityRender->expects($this->once()) + ->method('renderString') + ->with($userGroup, []) + ->willReturn('text'); + + $normalizer = new UserGroupNormalizer($entityRender); + + $actual = $normalizer->normalize($userGroup, 'json', [AbstractNormalizer::GROUPS => ['read']]); + + self::assertEqualsCanonicalizing([ + 'type' => 'user_group', + 'text' => 'text', + 'label' => ['fr' => 'test'], + 'excludeKey' => 'top', + 'foregroundColor' => '#123456', + 'backgroundColor' => '#456789', + 'id' => null, + ], $actual); + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss index d1faf5614..cec272a3a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss @@ -238,6 +238,10 @@ div[class*='budget-'] { background-color: $chill-ll-gray; color: $chill-blue; } + &.bg-user-group { + background-color: $chill-l-gray; + color: $chill-blue; + } &.bg-confidential { background-color: $chill-ll-gray; color: $chill-red; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue index b605cd5b5..ec9556a95 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue @@ -27,6 +27,11 @@ v-bind:item="item"> + + > + @@ -41,6 +46,7 @@ import SuggestionPerson from './TypePerson'; import SuggestionThirdParty from './TypeThirdParty'; import SuggestionUser from './TypeUser'; import SuggestionHousehold from './TypeHousehold'; +import SuggestionUserGroup from './TypeUserGroup'; export default { name: 'PersonSuggestion', @@ -49,6 +55,7 @@ export default { SuggestionThirdParty, SuggestionUser, SuggestionHousehold, + SuggestionUserGroup, }, props: [ 'item', diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue new file mode 100644 index 000000000..f1a83ff22 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue @@ -0,0 +1,30 @@ + + + + + From 17f4c85fa5d855184cf63eb56b5cbfe5dcb4ef07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 15:35:40 +0200 Subject: [PATCH 04/19] Add user group support in entity workflow steps Enhanced the WorkflowTransitionContextDTO to include user groups alongside individual users for future steps. Updated the relevant entity and form classes to accommodate this change and included the necessary database migration script. --- .../Entity/Workflow/EntityWorkflow.php | 6 ++- .../Entity/Workflow/EntityWorkflowStep.php | 25 ++++++++++++ .../ChillMainBundle/Form/WorkflowStepType.php | 3 +- .../Workflow/WorkflowTransitionContextDTO.php | 25 ++++++++++-- .../migrations/Version20240926132856.php | 39 +++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240926132856.php diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 89efde7c0..d25d20d6a 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -442,10 +442,14 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface $newStep->addCcUser($user); } - foreach ($transitionContextDTO->futureDestUsers as $user) { + foreach ($transitionContextDTO->getFutureDestUsers() as $user) { $newStep->addDestUser($user); } + foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) { + $newStep->addDestUserGroup($userGroup); + } + if (null !== $transitionContextDTO->futureUserSignature) { $newStep->addDestUser($transitionContextDTO->futureUserSignature); } diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index c6a849da5..419b42a83 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity\Workflow; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -48,6 +49,13 @@ class EntityWorkflowStep #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] private Collection $destUser; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: UserGroup::class)] + #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')] + private Collection $destUserGroups; + /** * @var Collection */ @@ -108,6 +116,7 @@ class EntityWorkflowStep { $this->ccUser = new ArrayCollection(); $this->destUser = new ArrayCollection(); + $this->destUserGroups = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection(); $this->signatures = new ArrayCollection(); $this->holdsOnStep = new ArrayCollection(); @@ -141,6 +150,22 @@ class EntityWorkflowStep return $this; } + public function addDestUserGroup(UserGroup $userGroup): self + { + if (!$this->destUserGroups->contains($userGroup)) { + $this->destUserGroups[] = $userGroup; + } + + return $this; + } + + public function removeDestUserGroup(UserGroup $userGroup): self + { + $this->destUserGroups->removeElement($userGroup); + + return $this; + } + public function addDestUserByAccessKey(User $user): self { if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) { diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index e2aece468..8ab17e1e1 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\PersonBundle\Form\Type\PickPersonDynamicType; @@ -156,7 +157,7 @@ class WorkflowStepType extends AbstractType 'label' => 'workflow.signature_zone.user signature', 'multiple' => false, ]) - ->add('futureDestUsers', PickUserDynamicType::class, [ + ->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [ 'label' => 'workflow.dest for next steps', 'multiple' => true, 'empty_data' => '[]', diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php index f675b9646..cc22d614a 100644 --- a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\PersonBundle\Entity\Person; use Symfony\Component\Validator\Constraints as Assert; @@ -24,25 +25,25 @@ use Symfony\Component\Workflow\Transition; class WorkflowTransitionContextDTO { /** - * a list of future dest users for the next steps. + * a list of future dest users or user groups for the next step. * * This is in used in order to let controller inform who will be the future users which will validate * the next step. This is necessary to perform some computation about the next users, before they are * associated to the entity EntityWorkflowStep. * - * @var array|User[] + * @var list */ public array $futureDestUsers = []; /** - * a list of future cc users for the next steps. + * a list of future cc users for the next step. * * @var array|User[] */ public array $futureCcUsers = []; /** - * a list of future dest emails for the next steps. + * a list of future dest emails for the next step. * * This is in used in order to let controller inform who will be the future emails which will validate * the next step. This is necessary to perform some computation about the next emails, before they are @@ -72,6 +73,22 @@ class WorkflowTransitionContextDTO public EntityWorkflow $entityWorkflow, ) {} + /** + * @return list + */ + public function getFutureDestUsers(): array + { + return array_values(array_filter($this->futureDestUsers, fn (User|UserGroup $user) => $user instanceof User)); + } + + /** + * @return list + */ + public function getFutureDestUserGroups(): array + { + return array_values(array_filter($this->futureDestUsers, fn (User|UserGroup $user) => $user instanceof UserGroup)); + } + #[Assert\Callback()] public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void { diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240926132856.php b/src/Bundle/ChillMainBundle/migrations/Version20240926132856.php new file mode 100644 index 000000000..e14bf7e78 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240926132856.php @@ -0,0 +1,39 @@ +addSql('CREATE TABLE chill_main_workflow_entity_step_user_group (entityworkflowstep_id INT NOT NULL, usergroup_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, usergroup_id))'); + $this->addSql('CREATE INDEX IDX_AB433F907E6AF9D4 ON chill_main_workflow_entity_step_user_group (entityworkflowstep_id)'); + $this->addSql('CREATE INDEX IDX_AB433F90D2112630 ON chill_main_workflow_entity_step_user_group (usergroup_id)'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group ADD CONSTRAINT FK_AB433F907E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group ADD CONSTRAINT FK_AB433F90D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group DROP CONSTRAINT FK_AB433F907E6AF9D4'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group DROP CONSTRAINT FK_AB433F90D2112630'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step_user_group'); + } +} From 86ec6f82dacf4b1c7a68e9505bf9403a67958a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 16:04:53 +0200 Subject: [PATCH 05/19] Do not block transition in EntityWorkflow when the user is member of a dest user group - refactor EntityWorkflowGuardTransition + tests - allow to find easily user within userGroup by adding a dedicated method to UserGroup::contains --- .../ChillMainBundle/Entity/UserGroup.php | 5 +++++ .../Entity/Workflow/EntityWorkflowStep.php | 12 +++++++++++- .../EntityWorkflowGuardTransitionTest.php | 10 ++++++++++ .../EntityWorkflowGuardTransition.php | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 588013b65..dddd01c70 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -141,4 +141,9 @@ class UserGroup { return true; } + + public function contains(User $user): bool + { + return $this->users->contains($user); + } } diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index 419b42a83..b64d61b71 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -203,7 +203,9 @@ class EntityWorkflowStep /** * get all the users which are allowed to apply a transition: those added manually, and - * those added automatically bu using an access key. + * those added automatically by using an access key. + * + * This method exclude the users associated with user groups * * @psalm-suppress DuplicateArrayKey */ @@ -217,6 +219,14 @@ class EntityWorkflowStep ); } + /** + * @return Collection + */ + public function getDestUserGroups(): Collection + { + return $this->destUserGroups; + } + public function getCcUser(): Collection { return $this->ccUser; diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php index dd3b12e86..eaef9b0c2 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Workflow\EventSubscriber; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter; use Chill\MainBundle\Templating\Entity\UserRender; @@ -145,6 +146,11 @@ class EntityWorkflowGuardTransitionTest extends TestCase yield [self::buildEntityWorkflow([new User()]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, false, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, true, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; + + $userGroup = new UserGroup(); + $userGroup->addUser(new User()); + + yield [self::buildEntityWorkflow([$userGroup]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc']; } public static function provideValidTransition(): iterable @@ -159,6 +165,10 @@ class EntityWorkflowGuardTransitionTest extends TestCase // transition allowed thanks to permission "apply all transitions" yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), true, 'step1']; yield [self::buildEntityWorkflow([new User()]), 'transition2', new User(), true, 'step2']; + + $userGroup = new UserGroup(); + $userGroup->addUser($u = new User()); + yield [self::buildEntityWorkflow([$userGroup]), 'transition1', $u, false, 'step1']; } public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php index 1c9d26fc7..5f9e0bca5 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php @@ -87,6 +87,17 @@ class EntityWorkflowGuardTransition implements EventSubscriberInterface return; } + if (!$user instanceof User) { + $event->addTransitionBlocker( + new TransitionBlocker( + 'workflow.Only regular user can apply a transition', + '04fb4f76-7c0e-11ef-afc3-877bad7b0fe7' + ) + ); + + return; + } + // for users if (!in_array('only-dest', $systemTransitions, true)) { $event->addTransitionBlocker( @@ -108,6 +119,13 @@ class EntityWorkflowGuardTransition implements EventSubscriberInterface return; } + // we give a second chance, searching for the presence of the user within userGroups + foreach ($entityWorkflow->getCurrentStep()->getDestUserGroups() as $userGroup) { + if ($userGroup->contains($user)) { + return; + } + } + $event->addTransitionBlocker(new TransitionBlocker( 'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc', From 87599425dfe557a107f0d6e5763999906b029401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 17:29:27 +0200 Subject: [PATCH 06/19] Send a notification to all User which are members of UserGroups, when a workflow is sent to them --- .../NotificationOnTransitionTest.php | 33 ++++++++----------- .../NotificationOnTransition.php | 14 +++++++- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php index fed7f2dce..e2fc30da8 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Tests\Workflow\EventSubscriber; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition; @@ -20,8 +21,6 @@ use Chill\MainBundle\Workflow\Helper\MetadataExtractor; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; -use Prophecy\Call\Call; -use Prophecy\Exception\Prediction\FailedPredictionException; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Security\Core\Security; use Symfony\Component\Workflow\Event\Event; @@ -57,29 +56,23 @@ final class NotificationOnTransitionTest extends TestCase $id->setValue($entityWorkflow, 1); $step = new EntityWorkflowStep(); + $userGroup = (new UserGroup())->addUser($userInGroup = new User())->addUser($dest); $entityWorkflow->addStep($step); - $step->addDestUser($dest) + $step + ->addDestUser($dest) + ->addDestUserGroup($userGroup) ->setCurrentStep('to_state'); $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->should( - static function ($args) use ($dest) { - /** @var Call[] $args */ - if (1 !== \count($args)) { - throw new FailedPredictionException('no notification sent'); - } - $notification = $args[0]->getArguments()[0]; - - if (!$notification instanceof Notification) { - throw new FailedPredictionException('persist is not a notification'); - } - - if (!$notification->getAddressees()->contains($dest)) { - throw new FailedPredictionException('the dest is not notified'); - } - } - ); + // we check that both notification has been persisted once, + // eliminating doublons + $em->persist(Argument::that( + fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($dest) + ))->shouldBeCalledOnce(); + $em->persist(Argument::that( + fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($userInGroup) + ))->shouldBeCalledOnce(); $engine = $this->prophesize(\Twig\Environment::class); $engine->render(Argument::type('string'), Argument::type('array')) diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php index d955d777f..89ae65f4c 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Workflow\EventSubscriber; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Workflow\Helper\MetadataExtractor; use Doctrine\ORM\EntityManagerInterface; @@ -69,7 +70,18 @@ class NotificationOnTransition implements EventSubscriberInterface // the dests for the current step $entityWorkflow->getCurrentStep()->getDestUser()->toArray(), // the cc users for the current step - $entityWorkflow->getCurrentStep()->getCcUser()->toArray() + $entityWorkflow->getCurrentStep()->getCcUser()->toArray(), + // the users within groups + $entityWorkflow->getCurrentStep()->getDestUserGroups()->reduce( + function (array $accumulator, UserGroup $userGroup) { + foreach ($userGroup->getUsers() as $user) { + $accumulator[] = $user; + } + + return $accumulator; + }, + [] + ), ) as $dest) { $dests[spl_object_hash($dest)] = $dest; } From 0c1d9ee4be43311c36df3807914540ad11a6536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 26 Sep 2024 17:44:31 +0200 Subject: [PATCH 07/19] Add support for handling user groups in workflow counters and list workflows in "my workflows" controller - rewrite queries in repositories; - fix cache key cleaning for members of users when a workflow is transitionned --- .../Workflow/EntityWorkflowRepository.php | 2 + .../Workflow/EntityWorkflowStepRepository.php | 7 ++- .../EntityWorkflowRepositoryTest.php | 11 +++++ .../EntityWorkflowStepRepositoryTest.php | 44 +++++++++++++++++++ .../Counter/WorkflowByUserCounter.php | 13 ++++++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowStepRepositoryTest.php diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 13c900ffb..cd39b6e28 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository\Workflow; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Doctrine\ORM\EntityManagerInterface; @@ -264,6 +265,7 @@ class EntityWorkflowRepository implements ObjectRepository $qb->expr()->orX( $qb->expr()->isMemberOf(':user', 'step.destUser'), $qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'), + $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF step.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class)) ), $qb->expr()->isNull('step.transitionAfter'), $qb->expr()->eq('step.isFinal', "'FALSE'") diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepRepository.php index db57836c2..bdf47357a 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository\Workflow; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -65,7 +66,11 @@ readonly class EntityWorkflowStepRepository implements ObjectRepository $qb->where( $qb->expr()->andX( - $qb->expr()->isMemberOf(':user', 'e.destUser'), + $qb->expr()->orX( + $qb->expr()->isMemberOf(':user', 'e.destUser'), + $qb->expr()->isMemberOf(':user', 'e.destUserByAccessKey'), + $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF e.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class)) + ), $qb->expr()->isNull('e.transitionAt'), $qb->expr()->eq('e.isFinal', ':bool'), ) diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowRepositoryTest.php index 4efb8196e..a26b29709 100644 --- a/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowRepositoryTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowRepositoryTest.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace ChillMainBundle\Tests\Repository; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -43,4 +44,14 @@ class EntityWorkflowRepositoryTest extends KernelTestCase self::assertIsArray($actual, 'check that the query is successful'); } + + public function testCountQueryByDest(): void + { + $repository = new EntityWorkflowRepository($this->em); + $user = $this->em->createQuery(sprintf('SELECT u FROM %s u', User::class)) + ->setMaxResults(1)->getSingleResult(); + $actual = $repository->countByDest($user); + + self::assertIsInt($actual); + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowStepRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowStepRepositoryTest.php new file mode 100644 index 000000000..572f7d66b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Repository/EntityWorkflowStepRepositoryTest.php @@ -0,0 +1,44 @@ +entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + public function testCountUnreadByUser(): void + { + $repository = new EntityWorkflowStepRepository($this->entityManager); + $user = $this->entityManager->createQuery(sprintf('SELECT u FROM %s u', User::class)) + ->setMaxResults(1)->getSingleResult(); + + $actual = $repository->countUnreadByUser($user); + + self::assertIsInt($actual); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Counter/WorkflowByUserCounter.php b/src/Bundle/ChillMainBundle/Workflow/Counter/WorkflowByUserCounter.php index a1b631265..8a6474034 100644 --- a/src/Bundle/ChillMainBundle/Workflow/Counter/WorkflowByUserCounter.php +++ b/src/Bundle/ChillMainBundle/Workflow/Counter/WorkflowByUserCounter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow\Counter; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository; use Chill\MainBundle\Templating\UI\NotificationCounterInterface; @@ -82,6 +83,18 @@ final readonly class WorkflowByUserCounter implements NotificationCounterInterfa foreach ($step->getDestUser() as $user) { $keys[] = self::generateCacheKeyWorkflowByUser($user); } + foreach ($step->getDestUserGroups()->reduce( + function (array $accumulator, UserGroup $userGroup) { + foreach ($userGroup->getUsers() as $user) { + $accumulator[] = $user; + } + + return $accumulator; + }, + [] + ) as $user) { + $keys[] = self::generateCacheKeyWorkflowByUser($user); + } if ([] !== $keys) { $this->cacheItemPool->deleteItems($keys); From 2e71808be1389ea84106aa5eb7c4f76a09834911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 27 Sep 2024 12:01:35 +0200 Subject: [PATCH 08/19] Add admin users and active status to UserGroup Added a new table `chill_main_user_group_user_admin` for admin users and modified the UserGroup entity to include an `active` status column. Included methods for managing the admin users and the active status in the UserGroup entity. --- .../ChillMainBundle/Entity/UserGroup.php | 45 +++++++++++++++++++ .../migrations/Version20240927095751.php | 34 ++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240927095751.php diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index dddd01c70..35e967e5f 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -24,6 +24,9 @@ class UserGroup #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] private ?int $id = null; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])] + private bool $active = true; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] private array $label = []; @@ -34,6 +37,13 @@ class UserGroup #[ORM\JoinTable(name: 'chill_main_user_group_user')] private Collection $users; + /** + * @var Collection&Selectable + */ + #[ORM\ManyToMany(targetEntity: User::class)] + #[ORM\JoinTable(name: 'chill_main_user_group_user_admin')] + private Collection&Selectable $adminUsers; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])] private string $backgroundColor = '#ffffffff'; @@ -54,6 +64,33 @@ class UserGroup $this->users = new ArrayCollection(); } + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + return $this; + } + + public function addAdminUser(User $user): self + { + if (!$this->adminUsers->contains($user)) { + $this->adminUsers[] = $user; + } + + return $this; + } + + public function removeAdminUser(User $user): self + { + $this->adminUsers->removeElement($user); + + return $this; + } + public function addUser(User $user): self { if (!$this->users->contains($user)) { @@ -87,6 +124,14 @@ class UserGroup return $this->users; } + /** + * @return Selectable&Collection + */ + public function getAdminUsers(): Collection&Selectable + { + return $this->adminUsers; + } + public function getForegroundColor(): string { return $this->foregroundColor; diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php b/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php new file mode 100644 index 000000000..ce34eb5c2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php @@ -0,0 +1,34 @@ +addSql('CREATE TABLE chill_main_user_group_user_admin (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))'); + $this->addSql('CREATE INDEX IDX_DAD75036D2112630 ON chill_main_user_group_user_admin (usergroup_id)'); + $this->addSql('CREATE INDEX IDX_DAD75036A76ED395 ON chill_main_user_group_user_admin (user_id)'); + $this->addSql('ALTER TABLE chill_main_user_group_user_admin ADD CONSTRAINT FK_DAD75036D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_user_group_user_admin ADD CONSTRAINT FK_DAD75036A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_user_group ADD active BOOLEAN DEFAULT true NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_user_group_user_admin DROP CONSTRAINT FK_DAD75036D2112630'); + $this->addSql('ALTER TABLE chill_main_user_group_user_admin DROP CONSTRAINT FK_DAD75036A76ED395'); + $this->addSql('DROP TABLE chill_main_user_group_user_admin'); + $this->addSql('ALTER TABLE chill_main_user_group DROP active'); + } +} From debca1f474fa0fc5248cbd049924d213e6e5a3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 27 Sep 2024 11:54:23 +0200 Subject: [PATCH 09/19] Admin CRUD for user groups --- .../Controller/UserGroupAdminController.php | 28 ++++++ .../ChillMainExtension.php | 24 ++++++ .../ChillMainBundle/Entity/UserGroup.php | 31 ++++++- .../ChillMainBundle/Form/UserGroupType.php | 58 +++++++++++++ .../Repository/UserGroupRepository.php | 3 +- .../public/chill/scss/flex_table.scss | 2 +- .../Resources/views/UserGroup/edit.html.twig | 21 +++++ .../Resources/views/UserGroup/index.html.twig | 86 +++++++++++++++++++ .../Resources/views/UserGroup/new.html.twig | 21 +++++ .../MenuBuilder/AdminUserMenuBuilder.php | 4 + .../migrations/Version20240927095751.php | 7 ++ .../translations/messages+intl-icu.fr.yaml | 9 ++ .../translations/messages.fr.yml | 20 +++++ 13 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/UserGroupAdminController.php create mode 100644 src/Bundle/ChillMainBundle/Form/UserGroupType.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/UserGroup/edit.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/UserGroup/index.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/UserGroup/new.html.twig diff --git a/src/Bundle/ChillMainBundle/Controller/UserGroupAdminController.php b/src/Bundle/ChillMainBundle/Controller/UserGroupAdminController.php new file mode 100644 index 000000000..04a9a9910 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/UserGroupAdminController.php @@ -0,0 +1,28 @@ +addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n') + ->setParameter('lang', $request->getLocale()); + $query->addOrderBy('labeli18n', 'ASC'); + + return $query; + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 97df404af..999277ed1 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -24,6 +24,7 @@ use Chill\MainBundle\Controller\LocationTypeController; use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\UserController; +use Chill\MainBundle\Controller\UserGroupAdminController; use Chill\MainBundle\Controller\UserGroupApiController; use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobController; @@ -70,6 +71,7 @@ use Chill\MainBundle\Form\LocationFormType; use Chill\MainBundle\Form\LocationTypeType; use Chill\MainBundle\Form\NewsItemType; use Chill\MainBundle\Form\RegroupmentType; +use Chill\MainBundle\Form\UserGroupType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType; @@ -355,6 +357,28 @@ class ChillMainExtension extends Extension implements { $container->prependExtensionConfig('chill_main', [ 'cruds' => [ + [ + 'class' => UserGroup::class, + 'controller' => UserGroupAdminController::class, + 'name' => 'admin_user_group', + 'base_path' => '/admin/main/user-group', + 'base_role' => 'ROLE_ADMIN', + 'form_class' => UserGroupType::class, + 'actions' => [ + 'index' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/UserGroup/index.html.twig', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/UserGroup/new.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/UserGroup/edit.html.twig', + ], + ], + ], [ 'class' => UserJob::class, 'controller' => UserJobController::class, diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 35e967e5f..b42609693 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -13,6 +13,10 @@ namespace Chill\MainBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Order; +use Doctrine\Common\Collections\ReadableCollection; +use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] @@ -31,11 +35,11 @@ class UserGroup private array $label = []; /** - * @var \Doctrine\Common\Collections\Collection + * @var Collection&Selectable */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_user_group_user')] - private Collection $users; + private Collection&Selectable $users; /** * @var Collection&Selectable @@ -61,6 +65,7 @@ class UserGroup public function __construct() { + $this->adminUsers = new \Doctrine\Common\Collections\ArrayCollection(); $this->users = new ArrayCollection(); } @@ -72,6 +77,7 @@ class UserGroup public function setActive(bool $active): self { $this->active = $active; + return $this; } @@ -119,7 +125,10 @@ class UserGroup return $this->label; } - public function getUsers(): Collection + /** + * @return Selectable&Collection + */ + public function getUsers(): Collection&Selectable { return $this->users; } @@ -191,4 +200,20 @@ class UserGroup { return $this->users->contains($user); } + + public function getUserListByLabelAscending(): ReadableCollection + { + $criteria = Criteria::create(); + $criteria->orderBy(['label' => Order::Ascending]); + + return $this->getUsers()->matching($criteria); + } + + public function getAdminUserListByLabelAscending(): ReadableCollection + { + $criteria = Criteria::create(); + $criteria->orderBy(['label' => Order::Ascending]); + + return $this->getAdminUsers()->matching($criteria); + } } diff --git a/src/Bundle/ChillMainBundle/Form/UserGroupType.php b/src/Bundle/ChillMainBundle/Form/UserGroupType.php new file mode 100644 index 000000000..8a4685c7c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/UserGroupType.php @@ -0,0 +1,58 @@ +add('label', TranslatableStringFormType::class, [ + 'label' => 'user_group.Label', + 'required' => true, + ]) + ->add('active') + ->add('backgroundColor', ColorType::class, [ + 'label' => 'user_group.BackgroundColor', + ]) + ->add('foregroundColor', ColorType::class, [ + 'label' => 'user_group.ForegroundColor', + ]) + ->add('excludeKey', TextType::class, [ + 'label' => 'user_group.ExcludeKey', + 'help' => 'user_group.ExcludeKeyHelp', + 'required' => false, + 'empty_data' => '', + ]) + ->add('users', PickUserDynamicType::class, [ + 'label' => 'user_group.Users', + 'multiple' => true, + 'required' => false, + 'empty_data' => [], + ]) + ->add('adminUsers', PickUserDynamicType::class, [ + 'label' => 'user_group.adminUsers', + 'multiple' => true, + 'required' => false, + 'empty_data' => [], + 'help' => 'user_group.adminUsersHelp', + ]) + ; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php index aa2d6f5f5..637846526 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php @@ -59,8 +59,9 @@ final readonly class UserGroupRepository implements UserGroupRepositoryInterface ->setSelectPertinence('3 + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) + CASE WHEN (EXISTS(SELECT 1 FROM unnest(string_to_array(label->>?, \' \')) AS t WHERE LOWER(t) LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')) THEN 100 ELSE 0 END', [$pattern, $lang, $lang, $pattern]) ->setFromClause('chill_main_user_group AS ug') ->setWhereClauses(' + ug.active AND ( SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) > 0.15 - OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\' + OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\') ', [$pattern, $lang, $pattern, $lang]); return $query; diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss index 673d5129a..0adf2b628 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss @@ -233,7 +233,7 @@ div.wrap-header { } &:last-child {} - div.wh-col { + & > div.wh-col { &:first-child { flex-grow: 0; flex-shrink: 1; flex-basis: auto; } diff --git a/src/Bundle/ChillMainBundle/Resources/views/UserGroup/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/edit.html.twig new file mode 100644 index 000000000..3090cb87c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/edit.html.twig @@ -0,0 +1,21 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/UserGroup/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/index.html.twig new file mode 100644 index 000000000..c9d666f76 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/index.html.twig @@ -0,0 +1,86 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block table_entities %} +
+ {% for entity in entities %} +
+
+
+
+
+ {{ entity|chill_entity_render_box }} +
+
+ {%- if not entity.active -%} +
+ {{ 'user_group.inactive'|trans }} +
  + {%- endif -%} +
{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}
+
+
+
+
+
+
+
+
+ {{ 'user_group.with_users'|trans }} +
+
+ {% for user in entity.userListByLabelAscending %} +

+ + {{ user|chill_entity_render_box }} + +

+ {% else %} +

{{ 'user_group.no_users'|trans }}

+ {% endfor %} +
+
+
+
+
+
+
+
+ {{ 'user_group.adminUsers'|trans }} +
+
+ {% for user in entity.adminUserListByLabelAscending %} +

+ + {{ user|chill_entity_render_box }} + +

+ {% else %} +

{{ 'user_group.no_admin_users'|trans }}

+ {% endfor %} +
+
+
+
+
+
    +
  • + +
  • +
+
+
+ {% endfor %} +
+ {% endblock %} + + {% block actions_before %} +
  • + {{'Back to the admin'|trans }} +
  • + {% endblock %} + + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/UserGroup/new.html.twig b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/new.html.twig new file mode 100644 index 000000000..2bb0c3d1f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/new.html.twig @@ -0,0 +1,21 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminUserMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminUserMenuBuilder.php index 22bdfbeb8..6c2baf0fd 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminUserMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminUserMenuBuilder.php @@ -67,6 +67,10 @@ class AdminUserMenuBuilder implements LocalMenuBuilderInterface 'route' => 'chill_crud_admin_user_index', ])->setExtras(['order' => 1040]); + $menu->addChild('crud.admin_user_group.index.title', [ + 'route' => 'chill_crud_admin_user_group_index', + ])->setExtras(['order' => 1042]); + $menu->addChild('User jobs', [ 'route' => 'chill_crud_admin_user_job_index', ])->setExtras(['order' => 1050]); diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php b/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php index ce34eb5c2..788efb1e8 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20240927095751.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * Chill is a software for social workers + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Chill\Migrations\Main; use Doctrine\DBAL\Schema\Schema; diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index 7f941b13a..ecfd0f5c8 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -5,6 +5,15 @@ years_old: >- other {# ans} } +user_group: + with_count_users: >- + {count, plural, + =0 {Aucun membre} + one {1 utilisateur} + many {# utilisateurs} + other {# utilisateurs} + } + notification: My notifications with counter: >- {nb, plural, diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 8bade3ce6..484e72a41 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -51,6 +51,20 @@ user: no job: Pas de métier assigné no scope: Pas de cercle assigné +user_group: + inactive: Inactif + with_users: Associé aux utilisateurs + no_users: Aucun utilisateur associé + no_admin_users: Aucun administrateur + Label: Nom du groupe + BackgroundColor: Couleur de fond du badge + ForegroundColor: Couleur de la police du badge + ExcludeKey: Clé d'exclusion + ExcludeKeyHelp: Lorsque cela est pertinent, les groupes comportant la même clé d'exclusion s'excluent mutuellement. + Users: Membres du groupe + adminUsers: Administrateurs du groupe + adminUsersHelp: Les administrateurs du groupe peuvent ajouter ou retirer des membres dans le groupe. + inactive: inactif Edit: Modifier @@ -395,6 +409,12 @@ crud: add_new: Créer title_new: Nouveau métier title_edit: Modifier un métier + admin_user_group: + index: + title: Groupes d'utilisateurs + add_new: Créer + title_edit: Modifier un groupe d'utilisateur + title_new: Nouveau groupe utilisateur main_location_type: index: title: Liste des types de localisations From 81706a61ef2d1be9addd68f08192db8a45a0863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Oct 2024 16:02:32 +0200 Subject: [PATCH 10/19] Fix issue with duplicate a tag --- .../ChillMainBundle/Resources/views/Layout/_header.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Layout/_header.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Layout/_header.html.twig index ab823ff2e..035b8278a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Layout/_header.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Layout/_header.html.twig @@ -60,7 +60,7 @@ data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> - {{ app.request.locale | capitalize }} + {{ app.request.locale | capitalize }} -
    - {{ form_row(transition_form.futureDestEmails) }} - {{ form_errors(transition_form.futureDestEmails) }} -

    {{ form_label(transition_form.comment) }}

    @@ -115,15 +111,6 @@ {% endif %} - {% if entity_workflow.currentStep.destEmail|length > 0 %} -

    {{ 'workflow.An access key was also sent to those addresses'|trans }} :

    -
      - {% for e in entity_workflow.currentStep.destEmail -%} -
    • {{ e }}
    • - {%- endfor %} -
    - {% endif %} - {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}

    {{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

      diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key.fr.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key.fr.txt.twig deleted file mode 100644 index b4574567d..000000000 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key.fr.txt.twig +++ /dev/null @@ -1,15 +0,0 @@ -Madame, Monsieur, - -Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}. - -Titre du workflow: "{{ entityTitle }}". - -Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant: - -{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey, '_locale': fr})) }} - -Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape. - -Notez que vous devez disposer d'un compte utilisateur valide dans Chill. - -Cordialement, diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key_title.fr.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key_title.fr.txt.twig deleted file mode 100644 index b505a97ed..000000000 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_send_access_key_title.fr.txt.twig +++ /dev/null @@ -1 +0,0 @@ -Un suivi {{ workflow.text }} demande votre attention: {{ entityTitle }} diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php index 4922cab17..952528dda 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php @@ -44,7 +44,6 @@ class EntityWorkflowMarkingStoreTest extends TestCase $dto = new WorkflowTransitionContextDTO($workflow); $dto->futureCcUsers[] = $user1 = new User(); $dto->futureDestUsers[] = $user2 = new User(); - $dto->futureDestEmails[] = $email = 'test@example.com'; $markingStore->setMarking($workflow, new Marking(['foo' => 1]), [ 'context' => $dto, @@ -55,7 +54,6 @@ class EntityWorkflowMarkingStoreTest extends TestCase $currentStep = $workflow->getCurrentStep(); self::assertEquals('foo', $currentStep->getCurrentStep()); - self::assertContains($email, $currentStep->getDestEmail()); self::assertContains($user1, $currentStep->getCcUser()); self::assertContains($user2, $currentStep->getDestUser()); diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php deleted file mode 100644 index a7bae81d4..000000000 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php +++ /dev/null @@ -1,59 +0,0 @@ -getEntityWorkflow(); - - $place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow); - $workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow( - $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()) - ); - $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); - - foreach ($step->getDestEmail() as $emailAddress) { - $context = [ - 'entity_workflow' => $entityWorkflow, - 'dest' => $emailAddress, - 'place' => $place, - 'workflow' => $workflow, - 'entityTitle' => $handler->getEntityTitle($entityWorkflow), - ]; - - $email = new Email(); - $email - ->addTo($emailAddress) - ->subject($this->engine->render('@ChillMain/Workflow/workflow_send_access_key_title.fr.txt.twig', $context)) - ->text($this->engine->render('@ChillMain/Workflow/workflow_send_access_key.fr.txt.twig', $context)); - - $this->mailer->send($email); - } - } -} diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php index cc22d614a..370aef0cf 100644 --- a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -42,17 +42,6 @@ class WorkflowTransitionContextDTO */ public array $futureCcUsers = []; - /** - * a list of future dest emails for the next step. - * - * This is in used in order to let controller inform who will be the future emails which will validate - * the next step. This is necessary to perform some computation about the next emails, before they are - * associated to the entity EntityWorkflowStep. - * - * @var array|string[] - */ - public array $futureDestEmails = []; - /** * A list of future @see{Person} with will sign the next step. * diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index 5976cb8b0..8b4aaa7bf 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -42,17 +42,6 @@ services: arguments: $handlers: !tagged_iterator chill_main.workflow_handler - Chill\MainBundle\Workflow\EventSubscriber\SendAccessKeyEventSubscriber: - autoconfigure: true - autowire: true - tags: - - - name: 'doctrine.orm.entity_listener' - event: 'postPersist' - entity: 'Chill\MainBundle\Entity\Workflow\EntityWorkflowStep' - # set the 'lazy' option to TRUE to only instantiate listeners when they are used - lazy: true - # other stuffes chill.main.helper.translatable_string: From d8ad8c36052086f26a3709175ddbfbed1f2e5968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Oct 2024 17:14:58 +0200 Subject: [PATCH 16/19] Add previous exception to the stack when denormlizing Discriminated Object --- .../Serializer/Normalizer/DiscriminatedObjectDenormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php index 1ecb7824f..12b7a908f 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php @@ -47,7 +47,7 @@ class DiscriminatedObjectDenormalizer implements ContextAwareDenormalizerInterfa } } - throw new RuntimeException(sprintf('Could not find any denormalizer for those ALLOWED_TYPES: %s', \implode(', ', $context[self::ALLOWED_TYPES]))); + throw new RuntimeException(sprintf('Could not find any denormalizer for those ALLOWED_TYPES: %s', \implode(', ', $context[self::ALLOWED_TYPES])), previous: $lastException ?? null); } public function supportsDenormalization($data, $type, $format = null, array $context = []) From c4c7280b5294cdadfe7d00bad612c2a60c5062bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Oct 2024 17:16:49 +0200 Subject: [PATCH 17/19] Fix Create a PickUserGroupOrUserDynamicType --- src/Bundle/ChillMainBundle/Entity/UserGroup.php | 5 ++++- .../Type/DataTransformer/EntityToJsonTransformer.php | 10 +++++++++- .../Form/Type/PickUserGroupOrUserDynamicType.php | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index b42609693..b39216b11 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -18,9 +18,12 @@ use Doctrine\Common\Collections\Order; use Doctrine\Common\Collections\ReadableCollection; use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; #[ORM\Entity] #[ORM\Table(name: 'chill_main_user_group')] +// this discriminator key is required for automated denormalization +#[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])] class UserGroup { #[ORM\Id] @@ -65,7 +68,7 @@ class UserGroup public function __construct() { - $this->adminUsers = new \Doctrine\Common\Collections\ArrayCollection(); + $this->adminUsers = new ArrayCollection(); $this->users = new ArrayCollection(); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php index 737c4683f..c6839efa8 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Form\Type\DataTransformer; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; +use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer; use Chill\PersonBundle\Entity\Person; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Symfony\Component\Form\DataTransformerInterface; @@ -76,15 +77,22 @@ class EntityToJsonTransformer implements DataTransformerInterface 'person' => Person::class, 'thirdparty' => ThirdParty::class, 'user_group' => UserGroup::class, + 'user_group_or_user' => DiscriminatedObjectDenormalizer::TYPE, default => throw new \UnexpectedValueException('This type is not supported'), }; + $context = [AbstractNormalizer::GROUPS => ['read']]; + + if ('user_group_or_user' === $this->type) { + $context[DiscriminatedObjectDenormalizer::ALLOWED_TYPES] = [UserGroup::class, User::class]; + } + return $this->denormalizer->denormalize( ['type' => $item['type'], 'id' => $item['id']], $class, 'json', - [AbstractNormalizer::GROUPS => ['read']], + $context, ); } } diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php b/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php index 00d783a97..c4e91cce4 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/PickUserGroupOrUserDynamicType.php @@ -30,7 +30,7 @@ final class PickUserGroupOrUserDynamicType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group')); + $builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group_or_user')); } public function buildView(FormView $view, FormInterface $form, array $options) From cef641ee245cf5b613e2d1e1656e61b434ff1ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Oct 2024 18:18:14 +0200 Subject: [PATCH 18/19] Render the history for workflow, with signature and onHold, and destUserGroups --- .../views/Workflow/_decision.html.twig | 15 ++++- .../views/Workflow/_history.html.twig | 40 ++++++++++---- .../views/Workflow/_signature.html.twig | 55 +------------------ .../views/Workflow/_signature_list.html.twig | 52 ++++++++++++++++++ .../Resources/views/Workflow/index.html.twig | 15 ----- .../Templating/Entity/UserGroupRender.php | 2 +- .../translations/messages+intl-icu.fr.yaml | 10 +++- 7 files changed, 106 insertions(+), 83 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_list.html.twig diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 575e7cb22..abad0801a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -88,7 +88,20 @@ {{ form_widget(transition_form.comment) }} -
        +
          + {% if entity_workflow.isOnHoldByUser(app.user) %} +
        • + + {{ 'workflow.Remove hold'|trans }} + +
        • + {% else %} +
        • + + {{ 'workflow.Put on hold'|trans }} + +
        • + {% endif %}
        • diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index 1a37baf27..d6e5e3216 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -13,22 +13,22 @@ {{ 'workflow.No transitions'|trans }} {% else %} -
          {% if step.previous is not null and step.previous.freezeAfter == true %} {% endif %} + {% if loop.last %} + {% if entity_workflow.isOnHoldAtCurrentStep %} + {% for hold in step.holdsOnStep %} + {{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string}) }} + {% endfor %} + {% endif %} + {% endif %}
          {{ place_label }}
          - {# -
          - - Refusé -
          - #}
          {% endif %} @@ -71,19 +71,33 @@ {% endif %} - {% if loop.last and step.allDestUser|length > 0 %} + {% if not loop.last and step.signatures|length > 0 %} +
          +
          +

          {{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :

          +
          + {{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }} +
          +
          +
          + {% endif %} + {% if loop.last and not step.isFinal %}
          - {% if step.destUser|length > 0 %} + {% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}

          {{ 'workflow.Users allowed to apply transition'|trans }} :

            {% for u in step.destUser %} -
          • {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }} - {% if entity_workflow.isOnHoldAtCurrentStep %} +
          • + {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }} + {% if step.isOnHoldByUser(u) %} {{ 'workflow.On hold'|trans }} {% endif %}
          • {% endfor %} + {% for u in step.destUserGroups %} +
          • {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
          • + {% endfor %}
          {% endif %} @@ -115,6 +129,10 @@ {% endif %}
          + {% if step.signatures|length > 0 %} +

          {{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :

          + {{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }} + {% endif %} {% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig index 42a3b68d8..300465016 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig @@ -1,54 +1,3 @@ -

          {{ 'workflow.signature_zone.title'|trans }}

          - -
          - {% for s in signatures %} -
          -
          - {% if s.signerKind == 'person' %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - action: 'show', displayBadge: true, - targetEntity: { name: 'person', id: s.signer.id }, - buttonText: s.signer|chill_entity_render_string, - isDead: s.signer.deathDate is not null - } %} - {% else %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - action: 'show', displayBadge: true, - targetEntity: { name: 'user', id: s.signer.id }, - buttonText: s.signer|chill_entity_render_string, - } %} - {% endif %} -
          -
          - {% if s.isSigned %} - {{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }} - {% elseif s.isCanceled %} - {{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }} - {% elseif s.isRejected%} - {{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }} - {% else %} - {% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %} - - {% endif %} - {% endif %} -
          -
          - {% endfor %} -
          +

          {{ 'workflow.signature_required_title'|trans({'nb_signatures': signatures|length}) }}

          +{{ include('@ChillMain/Workflow/_signature_list.html.twig') }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_list.html.twig new file mode 100644 index 000000000..e72cb0ba3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_list.html.twig @@ -0,0 +1,52 @@ +
          + {% for s in signatures %} +
          +
          + {% if s.signerKind == 'person' %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + action: 'show', displayBadge: true, + targetEntity: { name: 'person', id: s.signer.id }, + buttonText: s.signer|chill_entity_render_string, + isDead: s.signer.deathDate is not null + } %} + {% else %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + action: 'show', displayBadge: true, + targetEntity: { name: 'user', id: s.signer.id }, + buttonText: s.signer|chill_entity_render_string, + } %} + {% endif %} +
          +
          + {% if s.isSigned %} + {{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }} + {% elseif s.isCanceled %} + {{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }} + {% elseif s.isRejected%} + {{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }} + {% else %} + {% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %} + + {% endif %} + {% endif %} +
          +
          + {% endfor %} +
          + diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig index c5daff201..43d225796 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -67,21 +67,6 @@
          {% include '@ChillMain/Workflow/_comment.html.twig' %}
          #}
          {% include '@ChillMain/Workflow/_history.html.twig' %}
          - {% endblock %} diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php index b63fca8cf..f5a3c78aa 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php @@ -28,7 +28,7 @@ final readonly class UserGroupRender implements UserGroupRenderInterface public function renderString($entity, array $options): string { /* @var $entity UserGroup */ - return $this->translatableStringHelper->localize($entity->getLabel()); + return (string) $this->translatableStringHelper->localize($entity->getLabel()); } public function supports(object $entity, array $options): bool diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index 79205862a..9100f8ab8 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -58,9 +58,15 @@ workflow: } signature: signed_statement: 'Signature appliquée le {datetime, date, short} à {datetime, time, short}' - rejected_statement: 'Signature rejectée le {datetime, date, short} à {datetime, time, short}' + rejected_statement: 'Signature rejetée le {datetime, date, short} à {datetime, time, short}' canceled_statement: 'Signature annulée le {datetime, date, short} à {datetime, time, short}' - + On hold by: En attente par {by} + signature_required_title: >- + {nb_signatures, plural, + =0 {Aucune signature demandée} + one {Signature demandée} + other {Signatures demandées} + } duration: minute: >- From 818d8003848105126dbc17bbd7324ee01bf92c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Oct 2024 18:37:38 +0200 Subject: [PATCH 19/19] In workflow index page, show signature or decision, not both --- .../ChillMainBundle/Resources/views/Workflow/index.html.twig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig index 43d225796..4decb6fec 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -62,9 +62,10 @@
          {% include '@ChillMain/Workflow/_follow.html.twig' %}
          {% if signatures|length > 0 %}
          {% include '@ChillMain/Workflow/_signature.html.twig' %}
          + {% else %} +
          {% include '@ChillMain/Workflow/_decision.html.twig' %}
          {% endif %} -
          {% include '@ChillMain/Workflow/_decision.html.twig' %}
          {# -
          {% include '@ChillMain/Workflow/_comment.html.twig' %}
          #} + {#
          {% include '@ChillMain/Workflow/_comment.html.twig' %}
          #}
          {% include '@ChillMain/Workflow/_history.html.twig' %}