mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 17:28:23 +00:00 
			
		
		
		
	Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles
This commit is contained in:
		
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250901-094055.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250901-094055.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Add external identifier for a Person | ||||
| time: 2025-09-01T09:40:55.990365093+02:00 | ||||
| custom: | ||||
|     Issue: "64" | ||||
|     SchemaChange: Add columns or tables | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250821-161902.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250821-161902.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fixed html layout of pages for recovering password | ||||
| time: 2025-08-21T16:19:02.058348298+02:00 | ||||
| custom: | ||||
|     Issue: "422" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -18,6 +18,9 @@ migrations/* | ||||
| templates/* | ||||
| translations/* | ||||
|  | ||||
| # we allow developers to add customization on their installation, without commiting it | ||||
| config/packages/dev/* | ||||
|  | ||||
| ###> symfony/framework-bundle ### | ||||
| /.env.local | ||||
| /.env.local.php | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| 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\CustomFieldsBundle\EntityRepository; | ||||
|  | ||||
| use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup; | ||||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
|  | ||||
| class CustomFieldsDefaultGroupRepository extends ServiceEntityRepository | ||||
| { | ||||
|     public function __construct(ManagerRegistry $registry) | ||||
|     { | ||||
|         parent::__construct($registry, CustomFieldsDefaultGroup::class); | ||||
|     } | ||||
|  | ||||
|     public function findOneByEntity(string $className): ?CustomFieldsDefaultGroup | ||||
|     { | ||||
|         return $this->findOneBy(['entity' => $className]); | ||||
|     } | ||||
| } | ||||
| @@ -127,3 +127,7 @@ services: | ||||
|         factory: ["@doctrine", getRepository] | ||||
|         arguments: | ||||
|             - "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option" | ||||
|  | ||||
|     Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|   | ||||
| @@ -170,13 +170,14 @@ div.banner { | ||||
|       font-weight: lighter; | ||||
|       font-size: 50%; | ||||
|       margin-left: 0.5em; | ||||
|       &:before { content: '(n°'; } | ||||
|       &:after { content: ')'; } | ||||
|  | ||||
|        &.same-size { | ||||
|            font-size: unset; | ||||
|            font-weight: unset; | ||||
|        } | ||||
|    } | ||||
|    span.age { | ||||
|       margin-left: 0.5em; | ||||
|       &:before { content: '('; } | ||||
|       &:after { content: ')'; } | ||||
|    } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -44,8 +44,6 @@ section.chill-entity { | ||||
|                     margin-left: 0.5em; | ||||
|                 } | ||||
|                 span.id-number { | ||||
|                     &:before { content: '(n°'; } | ||||
|                     &:after { content: ')'; } | ||||
|                 } | ||||
|             } | ||||
|             p.moreinfo {} | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| <header> | ||||
|     <nav class="navbar navbar-dark bg-primary navbar-expand-md"> | ||||
|         <div class="container-xxl"> | ||||
|  | ||||
|             <div class="col-12"> | ||||
|                 <a class="navbar-brand" href="{{ path('chill_main_homepage') }}"> | ||||
|                     {{ include('@ChillMain/Layout/_header-logo.html.twig') }} | ||||
|                 </a> | ||||
|             </div> | ||||
|  | ||||
|         </div> | ||||
|     </nav> | ||||
| </header> | ||||
| @@ -26,11 +26,12 @@ | ||||
|  | ||||
|         {{ 'Welcome' | trans }}<br/> | ||||
|  | ||||
|         <b> | ||||
|             {{ app.user.username }} | ||||
|             {{ render(controller('Chill\\MainBundle\\Controller\\UIController::showNotificationUserCounterAction')) }} | ||||
|         </b> | ||||
|  | ||||
|         {% if app.user %} | ||||
|             <b> | ||||
|                 {{ app.user.username }} | ||||
|                 {{ render(controller('Chill\\MainBundle\\Controller\\UIController::showNotificationUserCounterAction')) }} | ||||
|             </b> | ||||
|         {% endif %} | ||||
|         {% if is_granted('IS_IMPERSONATOR') %} | ||||
|             <i class="fa fa-wrench fa-lg" title="Impersonate mode"></i> | ||||
|         {% endif %} | ||||
|   | ||||
| @@ -16,29 +16,16 @@ | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| #} | ||||
|  | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|     <head> | ||||
|         <meta charset="UTF-8" /> | ||||
|         <title> | ||||
|             {{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }} | ||||
|         </title> | ||||
|         <link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon"> | ||||
|         {{ encore_entry_link_tags('chill') }} | ||||
|     </head> | ||||
|     <body> | ||||
|         <header class="navigation container-fluid"> | ||||
|             <div class="col-4 d-md-none parent"> | ||||
|                 <div class="col-10 col-md-12 offset-2 logo-container"> | ||||
|                     <a href="{{ path('chill_main_homepage') }}"> | ||||
|                         <img class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}"> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </header> | ||||
| {% extends "@ChillMain/layout.html.twig" %} | ||||
|  | ||||
|         <div id="content"> | ||||
|             {% block content %}{% endblock %} | ||||
|         </div> | ||||
|     </body> | ||||
| </html> | ||||
| {% set header_logo_only = 1 %} | ||||
|  | ||||
| {% block title %}{{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }}{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
|     <div id="content"> | ||||
|         {% block password_content %}{% endblock %} | ||||
|     </div> | ||||
|  | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| {% block title %}{{ "New password set"|trans }}{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| {% block password_content %} | ||||
|     <div class="col-10 centered"> | ||||
|  | ||||
|         <h1>{{ "New password set"|trans }}</h1> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|  | ||||
| {% block title %}{{ title }}{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| {% block password_content %} | ||||
|     <div class="col-10 centered"> | ||||
|  | ||||
|         <h1>{{ title }}</h1> | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| {% block title %}{{"Recover password"|trans}}{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| {% block password_content %} | ||||
|     <div class="col-10 centered"> | ||||
|  | ||||
|         <h1>{{ 'Recover password'|trans }}</h1> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| {% block title "Check your email"|trans %} | ||||
|  | ||||
| {% block content %} | ||||
| {% block password_content %} | ||||
|  | ||||
| <div class="col-10 centered"> | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,11 @@ | ||||
|         {{ include('@ChillMain/Layout/_debug.html.twig') }} | ||||
|     {% endif %} | ||||
|  | ||||
|     {{ include('@ChillMain/Layout/_header.html.twig') }} | ||||
|     {% if header_logo_only is defined and header_logo_only == 1 %} | ||||
|         {{ include('@ChillMain/Layout/_header_logo_only.html.twig') }} | ||||
|     {% else %} | ||||
|         {{ include('@ChillMain/Layout/_header.html.twig') }} | ||||
|     {% endif %} | ||||
|  | ||||
|     {% block top_banner %}{# | ||||
|         To use if you want to add a banner below the header (ie the menu) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterfac | ||||
| use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface; | ||||
| use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass; | ||||
| use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface; | ||||
| use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface; | ||||
| use Chill\PersonBundle\Widget\PersonListWidgetFactory; | ||||
| use Symfony\Component\DependencyInjection\ContainerBuilder; | ||||
| @@ -38,5 +39,7 @@ class ChillPersonBundle extends Bundle | ||||
|             ->addTag('chill_person.list_person_customizer'); | ||||
|         $container->registerForAutoconfiguration(NotificationFlagProviderInterface::class) | ||||
|             ->addTag('chill_main.notification_flag_provider'); | ||||
|         $container->registerForAutoconfiguration(PersonIdentifierEngineInterface::class) | ||||
|             ->addTag('chill_person.person_identifier_engine'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,7 +17,6 @@ use Chill\PersonBundle\Entity\Household\Household; | ||||
| use Chill\PersonBundle\Entity\Household\HouseholdMember; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Form\CreationPersonType; | ||||
| use Chill\PersonBundle\Form\PersonType; | ||||
| use Chill\PersonBundle\Privacy\PrivacyEvent; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use Chill\PersonBundle\Search\SimilarPersonMatcher; | ||||
| @@ -49,56 +48,6 @@ final class PersonController extends AbstractController | ||||
|         private readonly EntityManagerInterface $em, | ||||
|     ) {} | ||||
|  | ||||
|     #[Route(path: '/{_locale}/person/{person_id}/general/edit', name: 'chill_person_general_edit')] | ||||
|     public function editAction(int $person_id, Request $request) | ||||
|     { | ||||
|         $person = $this->_getPerson($person_id); | ||||
|  | ||||
|         if (null === $person) { | ||||
|             throw $this->createNotFoundException(); | ||||
|         } | ||||
|  | ||||
|         $this->denyAccessUnlessGranted( | ||||
|             'CHILL_PERSON_UPDATE', | ||||
|             $person, | ||||
|             'You are not allowed to edit this person' | ||||
|         ); | ||||
|  | ||||
|         $form = $this->createForm( | ||||
|             PersonType::class, | ||||
|             $person, | ||||
|             [ | ||||
|                 'cFGroup' => $this->getCFGroup(), | ||||
|             ] | ||||
|         ); | ||||
|  | ||||
|         $form->handleRequest($request); | ||||
|  | ||||
|         if ($form->isSubmitted() && !$form->isValid()) { | ||||
|             $this->get('session') | ||||
|                 ->getFlashBag()->add('error', $this->translator | ||||
|                 ->trans('This form contains errors')); | ||||
|         } elseif ($form->isSubmitted() && $form->isValid()) { | ||||
|             $this->em->flush(); | ||||
|  | ||||
|             $this->get('session')->getFlashBag() | ||||
|                 ->add( | ||||
|                     'success', | ||||
|                     $this->translator | ||||
|                         ->trans('The person data has been updated') | ||||
|                 ); | ||||
|  | ||||
|             return $this->redirectToRoute('chill_person_view', [ | ||||
|                 'person_id' => $person->getId(), | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         return $this->render( | ||||
|             '@ChillPerson/Person/edit.html.twig', | ||||
|             ['person' => $person, 'form' => $form->createView()] | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function getCFGroup() | ||||
|     { | ||||
|         $cFGroup = null; | ||||
|   | ||||
| @@ -0,0 +1,79 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Controller; | ||||
|  | ||||
| use Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Form\PersonType; | ||||
| use Chill\PersonBundle\Security\Authorization\PersonVoter; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; | ||||
| use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpFoundation\Session\Session; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Translation\TranslatableMessage; | ||||
| use Twig\Environment; | ||||
|  | ||||
| final readonly class PersonEditController | ||||
| { | ||||
|     public function __construct( | ||||
|         private Security $security, | ||||
|         private FormFactoryInterface $formFactory, | ||||
|         private CustomFieldsDefaultGroupRepository $customFieldsDefaultGroupRepository, | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private UrlGeneratorInterface $urlGenerator, | ||||
|         private Environment $twig, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * @ParamConverter("person", options={"id": "person_id"}) | ||||
|      */ | ||||
|     #[Route(path: '/{_locale}/person/{person_id}/general/edit', name: 'chill_person_general_edit')] | ||||
|     public function editAction(Person $person, Request $request, Session $session) | ||||
|     { | ||||
|         if (!$this->security->isGranted(PersonVoter::UPDATE, $person)) { | ||||
|             throw new AccessDeniedHttpException('You are not allowed to edit this person.'); | ||||
|         } | ||||
|  | ||||
|         $form = $this->formFactory->create( | ||||
|             PersonType::class, | ||||
|             $person, | ||||
|             ['cFGroup' => $this->customFieldsDefaultGroupRepository->findOneByEntity(Person::class)?->getCustomFieldsGroup()] | ||||
|         ); | ||||
|  | ||||
|         $form->handleRequest($request); | ||||
|  | ||||
|         if ($form->isSubmitted() && !$form->isValid()) { | ||||
|             $session | ||||
|                 ->getFlashBag()->add('error', new TranslatableMessage('This form contains errors')); | ||||
|         } elseif ($form->isSubmitted() && $form->isValid()) { | ||||
|             $this->entityManager->flush(); | ||||
|  | ||||
|             $session->getFlashBag()->add('success', new TranslatableMessage('The person data has been updated')); | ||||
|  | ||||
|             return new RedirectResponse( | ||||
|                 $this->urlGenerator->generate('chill_person_view', ['person_id' => $person->getId()]) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return new Response($this->twig->render('@ChillPerson/Person/edit.html.twig', [ | ||||
|             'form' => $form->createView(), | ||||
|             'person' => $person, | ||||
|         ])); | ||||
|     } | ||||
| } | ||||
| @@ -110,6 +110,24 @@ class Configuration implements ConfigurationInterface | ||||
|             ->end() | ||||
|             ->end() // children for 'person_fields', parent = array 'person_fields' | ||||
|             ->end() // person_fields, parent = children of root | ||||
|             ->arrayNode('person_render') | ||||
|             ->addDefaultsIfNotSet() | ||||
|             ->children() | ||||
|             ->scalarNode('id_content_text') | ||||
|             ->defaultValue('n°[[ person_id ]]') | ||||
|             ->info( | ||||
|                 <<<'EOF' | ||||
|                 The way we display the person's id. Variables availables: "[[ person_id ]]", or, for person's | ||||
|                 identifier: "[[ identifier_xx ]]" where xx is the identifier's definition's id. | ||||
|  | ||||
|                 There are also conditions available: "[[ if:identifier_yy ]] [[ identifier_yy ]] [[ endif:identifier_yy ]]" | ||||
|  | ||||
|                 Take care of keeping exactly one space between "[[" and the placeholder's content, and exactly one space before "]]" | ||||
|                 EOF | ||||
|             ) | ||||
|             ->end() | ||||
|             ->end() // end of person_render's children | ||||
|             ->end() // end of person_render | ||||
|             ->arrayNode('household_fields') | ||||
|             ->canBeDisabled() | ||||
|             ->children() | ||||
|   | ||||
| @@ -0,0 +1,83 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Entity\Identifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
|  | ||||
| #[ORM\Entity] | ||||
| #[ORM\Table(name: 'chill_person_identifier')] | ||||
| class PersonIdentifier | ||||
| { | ||||
|     #[ORM\Id] | ||||
|     #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] | ||||
|     #[ORM\GeneratedValue] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     #[ORM\ManyToOne(targetEntity: Person::class)] | ||||
|     #[ORM\JoinColumn(name: 'person_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||||
|     private ?Person $person = null; | ||||
|  | ||||
|     #[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] | ||||
|     private array $value = []; | ||||
|  | ||||
|     #[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '[]'])] | ||||
|     private string $canonical = ''; | ||||
|  | ||||
|     public function __construct( | ||||
|         #[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)] | ||||
|         #[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||||
|         private PersonIdentifierDefinition $definition, | ||||
|     ) {} | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function setPerson(?Person $person): self | ||||
|     { | ||||
|         $this->person = $person; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getPerson(): Person | ||||
|     { | ||||
|         return $this->person; | ||||
|     } | ||||
|  | ||||
|     public function getValue(): array | ||||
|     { | ||||
|         return $this->value; | ||||
|     } | ||||
|  | ||||
|     public function setValue(array $value): void | ||||
|     { | ||||
|         $this->value = $value; | ||||
|     } | ||||
|  | ||||
|     public function getCanonical(): string | ||||
|     { | ||||
|         return $this->canonical; | ||||
|     } | ||||
|  | ||||
|     public function setCanonical(string $canonical): void | ||||
|     { | ||||
|         $this->canonical = $canonical; | ||||
|     } | ||||
|  | ||||
|     public function getDefinition(): PersonIdentifierDefinition | ||||
|     { | ||||
|         return $this->definition; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,107 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Entity\Identifier; | ||||
|  | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
|  | ||||
| #[ORM\Entity] | ||||
| #[ORM\Table(name: 'chill_person_identifier_definition')] | ||||
| class PersonIdentifierDefinition | ||||
| { | ||||
|     #[ORM\Id] | ||||
|     #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] | ||||
|     #[ORM\GeneratedValue] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     #[ORM\Column(name: 'active', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])] | ||||
|     private bool $active = true; | ||||
|  | ||||
|     public function __construct( | ||||
|         #[ORM\Column(name: 'label', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] | ||||
|         private array $label, | ||||
|         #[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)] | ||||
|         private string $engine, | ||||
|         #[ORM\Column(name: 'is_searchable', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])] | ||||
|         private bool $isSearchable = false, | ||||
|         #[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])] | ||||
|         private bool $isEditableByUsers = false, | ||||
|         #[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] | ||||
|         private array $data = [], | ||||
|     ) {} | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getLabel(): array | ||||
|     { | ||||
|         return $this->label; | ||||
|     } | ||||
|  | ||||
|     public function setLabel(array $label): void | ||||
|     { | ||||
|         $this->label = $label; | ||||
|     } | ||||
|  | ||||
|     public function getEngine(): string | ||||
|     { | ||||
|         return $this->engine; | ||||
|     } | ||||
|  | ||||
|     public function setEngine(string $engine): void | ||||
|     { | ||||
|         $this->engine = $engine; | ||||
|     } | ||||
|  | ||||
|     public function isSearchable(): bool | ||||
|     { | ||||
|         return $this->isSearchable; | ||||
|     } | ||||
|  | ||||
|     public function setIsSearchable(bool $isSearchable): void | ||||
|     { | ||||
|         $this->isSearchable = $isSearchable; | ||||
|     } | ||||
|  | ||||
|     public function isEditableByUsers(): bool | ||||
|     { | ||||
|         return $this->isEditableByUsers; | ||||
|     } | ||||
|  | ||||
|     public function setIsEditableByUsers(bool $isEditableByUsers): void | ||||
|     { | ||||
|         $this->isEditableByUsers = $isEditableByUsers; | ||||
|     } | ||||
|  | ||||
|     public function isActive(): bool | ||||
|     { | ||||
|         return $this->active; | ||||
|     } | ||||
|  | ||||
|     public function setActive(bool $active): self | ||||
|     { | ||||
|         $this->active = $active; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getData(): array | ||||
|     { | ||||
|         return $this->data; | ||||
|     } | ||||
|  | ||||
|     public function setData(array $data): void | ||||
|     { | ||||
|         $this->data = $data; | ||||
|     } | ||||
| } | ||||
| @@ -31,6 +31,7 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint; | ||||
| use Chill\PersonBundle\Entity\Household\Household; | ||||
| use Chill\PersonBundle\Entity\Household\HouseholdMember; | ||||
| use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\Entity\Person\PersonCenterCurrent; | ||||
| use Chill\PersonBundle\Entity\Person\PersonCenterHistory; | ||||
| use Chill\PersonBundle\Entity\Person\PersonCurrentAddress; | ||||
| @@ -271,6 +272,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI | ||||
|     #[ORM\GeneratedValue(strategy: 'AUTO')] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)] | ||||
|     private Collection $identifiers; | ||||
|  | ||||
|     /** | ||||
|      * The person's last name. | ||||
|      */ | ||||
| @@ -418,6 +422,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI | ||||
|         $this->resources = new ArrayCollection(); | ||||
|         $this->centerHistory = new ArrayCollection(); | ||||
|         $this->signatures = new ArrayCollection(); | ||||
|         $this->identifiers = new ArrayCollection(); | ||||
|     } | ||||
|  | ||||
|     public function __toString(): string | ||||
| @@ -498,6 +503,24 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addIdentifier(PersonIdentifier $identifier): self | ||||
|     { | ||||
|         if (!$this->identifiers->contains($identifier)) { | ||||
|             $this->identifiers[] = $identifier; | ||||
|             $identifier->setPerson($this); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeIdentifier(PersonIdentifier $identifier): self | ||||
|     { | ||||
|         $this->identifiers->removeElement($identifier); | ||||
|         $identifier->setPerson(null); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeSignature(EntityWorkflowStepSignature $signature): self | ||||
|     { | ||||
|         $this->signatures->removeElement($signature); | ||||
| @@ -1129,6 +1152,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return ReadableCollection<int, PersonIdentifier> | ||||
|      */ | ||||
|     public function getIdentifiers(): ReadableCollection | ||||
|     { | ||||
|         return $this->identifiers; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return string | ||||
|      */ | ||||
| @@ -1262,6 +1293,22 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI | ||||
|         return $this->spokenLanguages; | ||||
|     } | ||||
|  | ||||
|     public function addSpokenLanguage(Language $language): self | ||||
|     { | ||||
|         if (!$this->spokenLanguages->contains($language)) { | ||||
|             $this->spokenLanguages->add($language); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeSpokenLanguage(Language $language): self | ||||
|     { | ||||
|         $this->spokenLanguages->removeElement($language); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getUpdatedAt(): ?\DateTimeInterface | ||||
|     { | ||||
|         return $this->updatedAt; | ||||
|   | ||||
| @@ -0,0 +1,73 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Form\DataMapper; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\PersonIdentifier\Exception\UnexpectedTypeException; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; | ||||
| use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Symfony\Component\Form\DataMapperInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
|  | ||||
| final readonly class PersonIdentifiersDataMapper implements DataMapperInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private PersonIdentifierManagerInterface $identifierManager, | ||||
|         private PersonIdentifierDefinitionRepository $identifierDefinitionRepository, | ||||
|     ) {} | ||||
|  | ||||
|     public function mapDataToForms($viewData, \Traversable $forms): void | ||||
|     { | ||||
|         if (!$viewData instanceof Collection) { | ||||
|             throw new UnexpectedTypeException($viewData, Collection::class); | ||||
|         } | ||||
|         /** @var array<string, FormInterface> $formsByKey */ | ||||
|         $formsByKey = iterator_to_array($forms); | ||||
|  | ||||
|         foreach ($this->identifierManager->getWorkers() as $worker) { | ||||
|             if (!$worker->getDefinition()->isEditableByUsers()) { | ||||
|                 continue; | ||||
|             } | ||||
|             $form = $formsByKey['identifier_'.$worker->getDefinition()->getId()]; | ||||
|             $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition()->getId() === $identifier->getId()); | ||||
|             if (null === $identifier) { | ||||
|                 $identifier = new PersonIdentifier($worker->getDefinition()); | ||||
|             } | ||||
|             $form->setData($identifier->getValue()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function mapFormsToData(\Traversable $forms, &$viewData): void | ||||
|     { | ||||
|         if (!$viewData instanceof Collection) { | ||||
|             throw new UnexpectedTypeException($viewData, Collection::class); | ||||
|         } | ||||
|  | ||||
|         foreach ($forms as $name => $form) { | ||||
|             $identifierId = (int) substr((string) $name, 11); | ||||
|             $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getId() === $identifierId); | ||||
|             $definition = $this->identifierDefinitionRepository->find($identifierId); | ||||
|             if (null === $identifier) { | ||||
|                 $identifier = new PersonIdentifier($definition); | ||||
|                 $viewData->add($identifier); | ||||
|             } | ||||
|             if (!$identifier->getDefinition()->isEditableByUsers()) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($definition); | ||||
|             $identifier->setValue($form->getData()); | ||||
|             $identifier->setCanonical($worker->canonicalizeValue($identifier->getValue())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| final class PersonIdentifiersType extends AbstractType | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly PersonIdentifierManagerInterface $identifierManager, | ||||
|         private readonly PersonIdentifiersDataMapper $identifiersDataMapper, | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|     ) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options): void | ||||
|     { | ||||
|         foreach ($this->identifierManager->getWorkers() as $worker) { | ||||
|             if (!$worker->getDefinition()->isEditableByUsers()) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $subBuilder = $builder->create( | ||||
|                 'identifier_'.$worker->getDefinition()->getId(), | ||||
|                 options: [ | ||||
|                     'compound' => true, | ||||
|                     'label' => $this->translatableStringHelper->localize($worker->getDefinition()->getLabel()), | ||||
|                 ] | ||||
|             ); | ||||
|             $worker->buildForm($subBuilder); | ||||
|             $builder->add($subBuilder); | ||||
|         } | ||||
|  | ||||
|         $builder->setDataMapper($this->identifiersDataMapper); | ||||
|     } | ||||
| } | ||||
| @@ -72,8 +72,8 @@ class PersonType extends AbstractType | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         $builder | ||||
|             ->add('firstName') | ||||
|             ->add('lastName') | ||||
|             ->add('firstName', TextType::class, ['empty_data' => '']) | ||||
|             ->add('lastName', TextType::class, ['empty_data' => '']) | ||||
|             ->add('birthdate', ChillDateType::class, [ | ||||
|                 'required' => false, | ||||
|             ]) | ||||
| @@ -101,7 +101,7 @@ class PersonType extends AbstractType | ||||
|  | ||||
|         if ('visible' === $this->config['memo']) { | ||||
|             $builder | ||||
|                 ->add('memo', ChillTextareaType::class, ['required' => false]); | ||||
|                 ->add('memo', ChillTextareaType::class, ['required' => false, 'empty_data' => '']); | ||||
|         } | ||||
|  | ||||
|         if ('visible' === $this->config['employment_status']) { | ||||
| @@ -118,6 +118,7 @@ class PersonType extends AbstractType | ||||
|             $builder->add('placeOfBirth', TextType::class, [ | ||||
|                 'required' => false, | ||||
|                 'attr' => ['style' => 'text-transform: uppercase;'], | ||||
|                 'empty_data' => '', | ||||
|             ]); | ||||
|  | ||||
|             $builder->get('placeOfBirth')->addModelTransformer(new CallbackTransformer( | ||||
| @@ -127,7 +128,9 @@ class PersonType extends AbstractType | ||||
|         } | ||||
|  | ||||
|         if ('visible' === $this->config['contact_info']) { | ||||
|             $builder->add('contactInfo', ChillTextareaType::class, ['required' => false]); | ||||
|             $builder->add('contactInfo', ChillTextareaType::class, [ | ||||
|                 'required' => false, 'empty_data' => '', 'label' => 'Notes on contact information', | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         if ('visible' === $this->config['phonenumber']) { | ||||
| @@ -152,12 +155,12 @@ class PersonType extends AbstractType | ||||
|                         'required' => false, | ||||
|                     ] | ||||
|                 ) | ||||
|                 ->add('acceptSMS', CheckboxType::class, [ | ||||
|                 ->add('acceptSms', CheckboxType::class, [ | ||||
|                     'required' => false, | ||||
|                 ]); | ||||
|         } | ||||
|  | ||||
|         $builder->add('otherPhoneNumbers', ChillCollectionType::class, [ | ||||
|         $builder->add('otherPhonenumbers', ChillCollectionType::class, [ | ||||
|             'entry_type' => PersonPhoneType::class, | ||||
|             'button_add_label' => 'Add new phone', | ||||
|             'button_remove_label' => 'Remove phone', | ||||
| @@ -173,12 +176,12 @@ class PersonType extends AbstractType | ||||
|  | ||||
|         if ('visible' === $this->config['email']) { | ||||
|             $builder | ||||
|                 ->add('email', EmailType::class, ['required' => false]); | ||||
|                 ->add('email', EmailType::class, ['required' => false, 'empty_data' => '']); | ||||
|         } | ||||
|  | ||||
|         if ('visible' === $this->config['acceptEmail']) { | ||||
|             $builder | ||||
|                 ->add('acceptEmail', CheckboxType::class, ['required' => false]); | ||||
|                 ->add('acceptEmail', CheckboxType::class, ['required' => false, 'empty_data' => '']); | ||||
|         } | ||||
|  | ||||
|         if ('visible' === $this->config['country_of_birth']) { | ||||
| @@ -222,6 +225,10 @@ class PersonType extends AbstractType | ||||
|                 ]); | ||||
|         } | ||||
|  | ||||
|         $builder->add('identifiers', PersonIdentifiersType::class, [ | ||||
|             'by_reference' => false, | ||||
|         ]); | ||||
|  | ||||
|         if ($options['cFGroup']) { | ||||
|             $builder | ||||
|                 ->add( | ||||
| @@ -232,10 +239,7 @@ class PersonType extends AbstractType | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param OptionsResolverInterface $resolver | ||||
|      */ | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     public function configureOptions(OptionsResolver $resolver): void | ||||
|     { | ||||
|         $resolver->setDefaults([ | ||||
|             'data_class' => Person::class, | ||||
| @@ -251,10 +255,7 @@ class PersonType extends AbstractType | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getBlockPrefix() | ||||
|     public function getBlockPrefix(): string | ||||
|     { | ||||
|         return 'chill_personbundle_person'; | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Exception; | ||||
|  | ||||
| class EngineNotFoundException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(string $name) | ||||
|     { | ||||
|         parent::__construct("Engine for EngineInterface not found: {$name}"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Exception; | ||||
|  | ||||
| class PersonIdentifierDefinitionNotFoundException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(int $id, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct("Person identifier definition not found by his id: {$id}", previous: $previous); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Exception; | ||||
|  | ||||
| class UnexpectedTypeException extends \InvalidArgumentException | ||||
| { | ||||
|     public function __construct(mixed $value, string $expectedType) | ||||
|     { | ||||
|         parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value))); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Identifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| final readonly class StringIdentifier implements PersonIdentifierEngineInterface | ||||
| { | ||||
|     public static function getName(): string | ||||
|     { | ||||
|         return 'chill-person-bundle.string-identifier'; | ||||
|     } | ||||
|  | ||||
|     public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string | ||||
|     { | ||||
|         return $value['content'] ?? ''; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void | ||||
|     { | ||||
|         $builder->add('content', TextType::class, ['label' => false]); | ||||
|     } | ||||
|  | ||||
|     public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string | ||||
|     { | ||||
|         return $identifier?->getValue()['content'] ?? ''; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| interface PersonIdentifierEngineInterface | ||||
| { | ||||
|     public static function getName(): string; | ||||
|  | ||||
|     public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string; | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void; | ||||
|  | ||||
|     public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string; | ||||
| } | ||||
| @@ -0,0 +1,65 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException; | ||||
| use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository; | ||||
|  | ||||
| final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private iterable $engines, | ||||
|         private PersonIdentifierDefinitionRepository $personIdentifierDefinitionRepository, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * Build PersonIdentifierWorker's for all active definition. | ||||
|      * | ||||
|      * @return list<PersonIdentifierWorker> | ||||
|      */ | ||||
|     public function getWorkers(): array | ||||
|     { | ||||
|         $workers = []; | ||||
|         foreach ($this->personIdentifierDefinitionRepository->findByActive() as $definition) { | ||||
|             try { | ||||
|                 $worker = $this->getEngine($definition->getEngine()); | ||||
|             } catch (EngineNotFoundException) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $workers[] = new PersonIdentifierWorker($worker, $definition); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return $workers; | ||||
|     } | ||||
|  | ||||
|     public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker | ||||
|     { | ||||
|         return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throw EngineNotFoundException | ||||
|      */ | ||||
|     private function getEngine(string $name): PersonIdentifierEngineInterface | ||||
|     { | ||||
|         foreach ($this->engines as $engine) { | ||||
|             if ($engine->getName() === $name) { | ||||
|                 return $engine; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         throw new EngineNotFoundException($name); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
|  | ||||
| interface PersonIdentifierManagerInterface | ||||
| { | ||||
|     /** | ||||
|      * Build PersonIdentifierWorker's for all active definition. | ||||
|      * | ||||
|      * @return list<PersonIdentifierWorker> | ||||
|      */ | ||||
|     public function getWorkers(): array; | ||||
|  | ||||
|     public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker; | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| final readonly class PersonIdentifierWorker | ||||
| { | ||||
|     public function __construct( | ||||
|         private PersonIdentifierEngineInterface $identifierEngine, | ||||
|         private PersonIdentifierDefinition $definition, | ||||
|     ) {} | ||||
|  | ||||
|     public function getIdentifierEngine(): PersonIdentifierEngineInterface | ||||
|     { | ||||
|         return $this->identifierEngine; | ||||
|     } | ||||
|  | ||||
|     public function getDefinition(): PersonIdentifierDefinition | ||||
|     { | ||||
|         return $this->definition; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder): void | ||||
|     { | ||||
|         $this->identifierEngine->buildForm($builder, $this->definition); | ||||
|     } | ||||
|  | ||||
|     public function canonicalizeValue(array $value): ?string | ||||
|     { | ||||
|         return $this->identifierEngine->canonicalizeValue($value, $this->definition); | ||||
|     } | ||||
|  | ||||
|     public function renderAsString(?PersonIdentifier $identifier): string | ||||
|     { | ||||
|         return $this->identifierEngine->renderAsString($identifier, $this->definition); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,65 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Rendering; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
|  | ||||
| final readonly class PersonIdRendering implements PersonIdRenderingInterface | ||||
| { | ||||
|     private string $idContentText; | ||||
|  | ||||
|     public function __construct( | ||||
|         ParameterBagInterface $parameterBag, | ||||
|         private PersonIdentifierManagerInterface $personIdentifierManager, | ||||
|     ) { | ||||
|         $this->idContentText = $parameterBag->get('chill_person')['person_render']['id_content_text']; | ||||
|     } | ||||
|  | ||||
|     public function renderPersonId(Person $person): string | ||||
|     { | ||||
|         $args = [ | ||||
|             '[[ person_id ]]' => $person->getId(), | ||||
|         ]; | ||||
|  | ||||
|         foreach ($person->getIdentifiers() as $identifier) { | ||||
|             if (!$identifier->getDefinition()->isActive()) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $key = 'identifier_'.$identifier->getDefinition()->getId(); | ||||
|  | ||||
|             $args | ||||
|                 += [ | ||||
|                     "[[ {$key} ]]" => $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition()) | ||||
|                         ->renderAsString($identifier), | ||||
|                     "[[ if:{$key} ]]" => '', | ||||
|                     "[[ endif:{$key} ]]" => '', | ||||
|                 ]; | ||||
|             // we remove the eventual conditions | ||||
|  | ||||
|  | ||||
|         } | ||||
|  | ||||
|         $rendered = strtr($this->idContentText, $args); | ||||
|  | ||||
|         // Delete the conditions which are not met, for instance: | ||||
|         // [[ if:identifier_99 ]] ... [[ endif:identifier_99 ]] | ||||
|         // this match the same dumber for opening and closing of the condition | ||||
|         return preg_replace( | ||||
|             '/\[\[\s*if:identifier_(\d+)\s*\]\].*?\[\[\s*endif:identifier_\1\s*\]\]/s', | ||||
|             '', | ||||
|             $rendered | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Rendering; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
|  | ||||
| interface PersonIdRenderingInterface | ||||
| { | ||||
|     public function renderPersonId(Person $person): string; | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Rendering; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Twig\Extension\AbstractExtension; | ||||
| use Twig\TwigFilter; | ||||
|  | ||||
| final class PersonIdRenderingTwigExtension extends AbstractExtension | ||||
| { | ||||
|     public function __construct(private readonly PersonIdRenderingInterface $personIdRendering) {} | ||||
|  | ||||
|     public function getFilters(): array | ||||
|     { | ||||
|         return [ | ||||
|             new TwigFilter( | ||||
|                 'chill_person_id_render_text', | ||||
|                 fn (Person $person): string => $this->personIdRendering->renderPersonId($person) | ||||
|             ), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\PersonIdentifier\Rendering; | ||||
|  | ||||
| use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; | ||||
|  | ||||
| /** | ||||
|  * @template-implements ChillEntityRenderInterface<PersonIdentifier> | ||||
|  */ | ||||
| final readonly class PersonIdentifierEntityRender implements ChillEntityRenderInterface | ||||
| { | ||||
|     public function __construct(private PersonIdentifierManagerInterface $identifierManager) {} | ||||
|  | ||||
|     public function renderBox(mixed $entity, array $options): string | ||||
|     { | ||||
|         return $this->renderString($entity, $options); | ||||
|     } | ||||
|  | ||||
|     public function renderString(mixed $entity, array $options): string | ||||
|     { | ||||
|         $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($entity->getDefinition()); | ||||
|  | ||||
|         return $worker->renderAsString($entity); | ||||
|     } | ||||
|  | ||||
|     public function supports(object $entity, array $options): bool | ||||
|     { | ||||
|         return $entity instanceof PersonIdentifier; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Repository\Identifier; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
|  | ||||
| /** | ||||
|  * @template-extends ServiceEntityRepository<PersonIdentifierDefinition> | ||||
|  */ | ||||
| class PersonIdentifierDefinitionRepository extends ServiceEntityRepository | ||||
| { | ||||
|     public function __construct(ManagerRegistry $managerRegistry) | ||||
|     { | ||||
|         parent::__construct($managerRegistry, PersonIdentifierDefinition::class); | ||||
|     } | ||||
|  | ||||
|     public function findByActive(): array | ||||
|     { | ||||
|         return $this->findBy(['active' => true]); | ||||
|     } | ||||
| } | ||||
| @@ -281,11 +281,6 @@ abbr.referrer {   // still used ? | ||||
|     font-style: italic; | ||||
| } | ||||
|  | ||||
| .created-updated { | ||||
|     border: 1px solid black; | ||||
|     padding: 10px; | ||||
| } | ||||
|  | ||||
| /// Masonry blocs on AccompanyingCourse resume page | ||||
| div#dashboards { | ||||
|     div.mbloc { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|                             <h1> | ||||
|                                 <i class="fa fa-random fa-fw"></i> | ||||
|                                 {{ 'Accompanying Course'|trans }} | ||||
|                                 <span class="id-number">{{ accompanyingCourse.id }}</span> | ||||
|                                 <span class="id-number">({{ 'accompanying_period.number'|trans({ 'id': accompanyingCourse.id}) }})</span> | ||||
|                             </h1> | ||||
|                         </div> | ||||
|                     </div> | ||||
|   | ||||
| @@ -78,11 +78,6 @@ | ||||
|             {%- if options['addEntity'] -%} | ||||
|                 <span class="badge rounded-pill bg-secondary">{{ 'Person'|trans }}</span> | ||||
|             {%- endif -%} | ||||
|             {%- if options['addId'] -%} | ||||
|                 <span class="id-number" title="{{ 'Person'|trans ~ ' n° ' ~ person.id }}"> | ||||
|                     {{ person.id|upper -}} | ||||
|                 </span> | ||||
|             {%- endif -%} | ||||
|         </div> | ||||
|         {%- if options['addInfo'] -%} | ||||
|             <p class="moreinfo"> | ||||
| @@ -99,6 +94,12 @@ | ||||
|                 {%- if options['addAge'] -%} | ||||
|                     <span class="age"> {{ 'years_old'|trans({ 'age': person.age }) }}</span> | ||||
|                 {%- endif -%} | ||||
|                 {%- if options['addId'] -%} | ||||
|                     {%- set personId = person|chill_person_id_render_text %} | ||||
|                     <span class="id-number" title="{{ 'Person'|trans ~ ' ' ~ personId }}"> | ||||
|                     ({{ personId }}) | ||||
|                 </span> | ||||
|                 {%- endif -%} | ||||
|             {%- elseif person.birthdate is not null -%} | ||||
|                 <time datetime="{{ person.birthdate|date('Y-m-d') }}" title="{{ 'Birthdate'|trans }}"> | ||||
|                 {{ 'Born the date'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral', | ||||
| @@ -108,6 +109,12 @@ | ||||
|                     <span class="age">{{- 'years_old'|trans({ 'age': person.age }) -}}</span> | ||||
|                 {%- endif -%} | ||||
|             {%- endif -%} | ||||
|             {%- if options['addId'] -%} | ||||
|                 {%- set personId = person|chill_person_id_render_text %} | ||||
|                 <span class="id-number same-size" title="{{ 'Person'|trans ~ ' ' ~ personId }}"> | ||||
|                     ({{ personId }}) | ||||
|                 </span> | ||||
|             {%- endif -%} | ||||
|             </p> | ||||
|         {%- endif -%} | ||||
|     {#- tricks to remove easily whitespace after template -#} | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
|     {% if form.memo is defined %} | ||||
|     <fieldset> | ||||
|         <legend><h2>{{ 'Memo'|trans }}</h2></legend> | ||||
|         {{ form_row(form.memo, {'label' : 'Memo'} ) }} | ||||
|         {{ form_widget(form.memo, {'label' : 'Memo'} ) }} | ||||
|     </fieldset> | ||||
|     {% endif %} | ||||
|  | ||||
| @@ -85,15 +85,17 @@ | ||||
|                     {{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }} | ||||
|                 </div> | ||||
|                 <div id="personAcceptSMS"> | ||||
|                     {{ form_row(form.acceptSMS, {'label' : 'Accept short text message ?'}) }} | ||||
|                     {{ form_row(form.acceptSms, {'label' : 'Accept short text message ?'}) }} | ||||
|                 </div> | ||||
|             {%- endif -%} | ||||
|             {%- if form.otherPhoneNumbers is defined -%} | ||||
|                 {{ form_widget(form.otherPhoneNumbers) }} | ||||
|                 {{ form_errors(form.otherPhoneNumbers) }} | ||||
|             {%- if form.otherPhonenumbers is defined -%} | ||||
|                 {{ form_widget(form.otherPhonenumbers) }} | ||||
|                 {{ form_errors(form.otherPhonenumbers) }} | ||||
|             {%- endif -%} | ||||
|             {%- if form.contactInfo is defined -%} | ||||
|                 {{ form_row(form.contactInfo, {'label': 'Notes on contact information'}) }} | ||||
|                 {{ form_label(form.contactInfo) }} | ||||
|                 {{ form_widget(form.contactInfo) }} | ||||
|                 {{ form_errors(form.contactInfo) }} | ||||
|             {%- endif -%} | ||||
|         </fieldset> | ||||
|     {%- endif -%} | ||||
| @@ -134,6 +136,20 @@ | ||||
|     </fieldset> | ||||
|     {%- endif -%} | ||||
|  | ||||
|     {% if form.identifiers|length > 0 %} | ||||
|         <fieldset> | ||||
|             <legend><h2>{{ 'person.Identifiers'|trans }}</h2></legend> | ||||
|             <div> | ||||
|                 {% for f in form.identifiers %} | ||||
|                     {{ form_row(f) }} | ||||
|                 {% endfor %} | ||||
|             </div> | ||||
|         </fieldset> | ||||
|     {% else %} | ||||
|         {{ form_widget(form.identifiers) }} | ||||
|     {% endif %} | ||||
|  | ||||
|  | ||||
|     {{ form_rest(form) }} | ||||
|  | ||||
|     <ul class="record_actions sticky-form-buttons"> | ||||
|   | ||||
| @@ -1,19 +1,3 @@ | ||||
| {# | ||||
|  * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop> | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  *  it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  *  License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| #} | ||||
| {% extends "@ChillPerson/Person/layout.html.twig"  %} | ||||
|  | ||||
| {% set activeRouteKey = 'chill_person_view' %} | ||||
| @@ -78,6 +62,16 @@ This view should receive those arguments: | ||||
|                     {% else %} | ||||
|                         <dd>{{ 'gender.not defined'|trans }}</dd> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     {% if person.genderComment.comment is not empty %} | ||||
|                         <dt>{{ 'Gender comment'|trans }} :</dt> | ||||
|                         <dd> | ||||
|                             <div class="chill-user-quote"> | ||||
|                                 {{ person.genderComment.comment|chill_markdown_to_html }} | ||||
|                             </div> | ||||
|                         </dd> | ||||
|                     {% endif %} | ||||
|  | ||||
|                 </dl> | ||||
|             </figure> | ||||
|         </div> | ||||
| @@ -126,16 +120,6 @@ This view should receive those arguments: | ||||
|             </figure> | ||||
|         </div> | ||||
|  | ||||
|         {% if person.genderComment.comment is not empty %} | ||||
|             <div class="col-12"> | ||||
|                 <figure class="person-details"> | ||||
|                     <h2 class="chill-beige">{{ 'Gender comment'|trans }} :</h2> | ||||
|                     <div class="chill-user-quote"> | ||||
|                         {{ person.genderComment.comment|chill_markdown_to_html }} | ||||
|                     </div> | ||||
|                 </figure> | ||||
|             </div> | ||||
|         {% endif %} | ||||
|     </div> | ||||
|  | ||||
|     <div class="row"> | ||||
| @@ -241,17 +225,20 @@ This view should receive those arguments: | ||||
|                             <span class="chill-no-data-statement">{{ 'No data given'|trans }}</span> | ||||
|                         {% endif %} | ||||
|                     </dd> | ||||
|                     <dt>{{ 'Comment on the marital status'|trans }} :</dt> | ||||
|  | ||||
|                     <dd> | ||||
|                         {% if person.maritalStatusComment.comment is not empty %} | ||||
|                             <blockquote class="chill-user-quote"> | ||||
|                                 {{ person.maritalStatusComment.comment|chill_markdown_to_html }} | ||||
|                             </blockquote> | ||||
|                         {% else %} | ||||
|                             <span class="chill-no-data-statement">{{ 'No data given'|trans }}</span> | ||||
|                     {% if person.maritalStatusComment.comment is not empty %} | ||||
|                         <dt>{{ 'Comment on the marital status'|trans }} :</dt> | ||||
|                         <dd> | ||||
|                                 <blockquote class="chill-user-quote"> | ||||
|                                     {{ person.maritalStatusComment.comment|chill_markdown_to_html }} | ||||
|                                 </blockquote> | ||||
|                         </dd> | ||||
|                     {% endif %} | ||||
|                     {% for identifier in person.identifiers %} | ||||
|                         {% if identifier.definition.isActive and (identifier|chill_entity_render_string) is not empty %} | ||||
|                             <dt>{{ identifier.definition.label|localize_translatable_string }} :</dt> | ||||
|                             <dd>{{ identifier|chill_entity_render_box }}</dd> | ||||
|                         {% endif %} | ||||
|                     </dd> | ||||
|                     {% endfor %} | ||||
|                 </dl> | ||||
|                 {%- endif -%} | ||||
|             </figure> | ||||
| @@ -341,7 +328,7 @@ This view should receive those arguments: | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <div class="created-updated"> | ||||
|     <div> | ||||
|         {% if person.createdBy %} | ||||
|             <div class="createdBy"> | ||||
|                 {{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box({'at_date': person.createdAt}) }}</b>,<br> | ||||
|   | ||||
| @@ -23,7 +23,11 @@ class PersonRender implements PersonRenderInterface | ||||
| { | ||||
|     use BoxUtilsChillEntityRenderTrait; | ||||
|  | ||||
|     public function __construct(private readonly ConfigPersonAltNamesHelper $configAltNamesHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {} | ||||
|     public function __construct( | ||||
|         private readonly ConfigPersonAltNamesHelper $configAltNamesHelper, | ||||
|         private readonly \Twig\Environment $engine, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|     ) {} | ||||
|  | ||||
|     public function renderBox($person, array $options): string | ||||
|     { | ||||
|   | ||||
| @@ -0,0 +1,145 @@ | ||||
| <?php | ||||
|  | ||||
| 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\PersonBundle\Tests\PersonIdentifier\Rendering; | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; | ||||
| use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\PersonIdentifier\Identifier\StringIdentifier; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; | ||||
| use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker; | ||||
| use Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRendering; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class PersonIdRenderingTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideRenderCases | ||||
|      */ | ||||
|     public function testRenderPersonId(Person $person, string $idContentText, string $expected): void | ||||
|     { | ||||
|         // Parameter bag mock returning the provided id_content_text | ||||
|         $parameterBag = $this->prophesize(ParameterBagInterface::class); | ||||
|         $parameterBag->get('chill_person') | ||||
|             ->willReturn(['person_render' => ['id_content_text' => $idContentText]]); | ||||
|  | ||||
|         // PersonIdentifierManager is explicitly requested to be mocked in the spec. | ||||
|         // It will return a PersonIdentifierWorker whose renderAsString behaves like StringIdentifier::renderAsString | ||||
|         $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); | ||||
|         $personIdentifierManager | ||||
|             ->buildWorkerByPersonIdentifierDefinition(Argument::type(PersonIdentifierDefinition::class)) | ||||
|             ->will(function ($args) { | ||||
|                 /** @var PersonIdentifierDefinition $definition */ | ||||
|                 $definition = $args[0]; | ||||
|  | ||||
|                 $engine = new class () implements PersonIdentifierEngineInterface { | ||||
|                     public static function getName(): string | ||||
|                     { | ||||
|                         return 'test'; | ||||
|                     } | ||||
|  | ||||
|                     public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string | ||||
|                     { | ||||
|                         return $value['content'] ?? ''; | ||||
|                     } | ||||
|  | ||||
|                     public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {} | ||||
|  | ||||
|                     public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string | ||||
|                     { | ||||
|                         // same behavior as StringIdentifier::renderAsString | ||||
|                         return $identifier?->getValue()['content'] ?? ''; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 return new PersonIdentifierWorker($engine, $definition); | ||||
|             }); | ||||
|  | ||||
|         $service = new PersonIdRendering($parameterBag->reveal(), $personIdentifierManager->reveal()); | ||||
|  | ||||
|         self::assertSame($expected, $service->renderPersonId($person)); | ||||
|     } | ||||
|  | ||||
|     public function provideRenderCases(): iterable | ||||
|     { | ||||
|         // Case 1: one active identifier, one inactive identifier, should render person id and only active identifier | ||||
|         $person1 = new Person(); | ||||
|         $this->setEntityId($person1, 123); | ||||
|  | ||||
|         $defActive = new PersonIdentifierDefinition(label: ['en' => 'Active'], engine: 'string'); | ||||
|         $this->setEntityId($defActive, 10); | ||||
|         $defActive->setActive(true); | ||||
|  | ||||
|         $idActive = new PersonIdentifier($defActive); | ||||
|         $idActive->setPerson($person1); | ||||
|         $idActive->setValue(['content' => 'ABC']); | ||||
|         $person1->addIdentifier($idActive); | ||||
|  | ||||
|         $defInactive = new PersonIdentifierDefinition(label: ['en' => 'Inactive'], engine: 'string'); | ||||
|         $this->setEntityId($defInactive, 99); | ||||
|         $defInactive->setActive(false); | ||||
|  | ||||
|         $idInactive = new PersonIdentifier($defInactive); | ||||
|         $idInactive->setPerson($person1); | ||||
|         $idInactive->setValue(['content' => 'SHOULD_NOT_APPEAR']); | ||||
|         $person1->addIdentifier($idInactive); | ||||
|  | ||||
|         $template1 = 'ID: [[ person_id ]] - Active: [[ identifier_10 ]] - Inactive: [[ identifier_99 ]]'; | ||||
|         $expected1 = 'ID: 123 - Active: ABC - Inactive: [[ identifier_99 ]]'; | ||||
|  | ||||
|         yield | ||||
|             'with active and inactive identifiers' => [$person1, $template1, $expected1] | ||||
|         ; | ||||
|  | ||||
|         $template2 = 'ID: [[ person_id ]][[ if:identifier_10 ]] - Active: [[ identifier_10 ]][[ endif:identifier_10 ]]'; | ||||
|         $expected2 = 'ID: 123 - Active: ABC'; | ||||
|  | ||||
|         yield | ||||
|             'rendering with conditional: condition are removed' => [$person1, $template2, $expected2] | ||||
|         ; | ||||
|  | ||||
|         $template3 = 'ID: [[ person_id ]][[ if:identifier_99 ]] - Inactive: [[ identifier_10 ]][[ endif:identifier_99 ]]'; | ||||
|         $expected3 = 'ID: 123'; | ||||
|  | ||||
|         yield | ||||
|             'rendering with conditional: the content between condition is removed' => [$person1, $template3, $expected3] | ||||
|         ; | ||||
|  | ||||
|         $template4 = 'ID: [[ person_id ]][[ if:identifier_105 ]] - not present: [[ identifier_105 ]][[ endif:identifier_105 ]]'; | ||||
|         $expected4 = 'ID: 123'; | ||||
|  | ||||
|         yield | ||||
|             'rendering with conditional: the content between condition is removed, the identifier is not associated with the person' => [$person1, $template4, $expected4] | ||||
|         ; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private function setEntityId(object $entity, int $id): void | ||||
|     { | ||||
|         $refl = new \ReflectionClass($entity); | ||||
|         $prop = $refl->getProperty('id'); | ||||
|         $prop->setAccessible(true); | ||||
|         $prop->setValue($entity, $id); | ||||
|     } | ||||
| } | ||||
| @@ -95,3 +95,16 @@ services: | ||||
|     Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodViewEntityInfoProvider: | ||||
|         arguments: | ||||
|             $unions: !tagged_iterator chill_person.accompanying_period_info_part | ||||
|  | ||||
|     Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager: | ||||
|         arguments: | ||||
|             $engines: !tagged_iterator chill_person.person_identifier_engine | ||||
|  | ||||
|     Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface: | ||||
|         alias: Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager | ||||
|  | ||||
|     Chill\PersonBundle\PersonIdentifier\Identifier\: | ||||
|         resource: '../PersonIdentifier/Identifier' | ||||
|  | ||||
|     Chill\PersonBundle\PersonIdentifier\Rendering\: | ||||
|         resource: '../PersonIdentifier/Rendering' | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| <?php | ||||
|  | ||||
| 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\Person; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20250822123819 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add person identifier tables: chill_person_identifier_definition and chill_person_identifier with FKs to person and definition; create supporting sequences and indexes.'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE SEQUENCE chill_person_identifier_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE SEQUENCE chill_person_identifier_definition_id_seq INCREMENT BY 1 MINVALUE 1 START 1000'); | ||||
|         $this->addSql( | ||||
|             <<<'SQL' | ||||
|                 CREATE TABLE chill_person_identifier ( | ||||
|                 id INT NOT NULL, | ||||
|                 person_id INT NOT NULL, | ||||
|                 definition_id INT NOT NULL, | ||||
|                 value JSONB NOT NULL DEFAULT '[]'::jsonb, | ||||
|                 canonical TEXT NOT NULL DEFAULT '', | ||||
|                 PRIMARY KEY(id) | ||||
|                 ) | ||||
|                 SQL | ||||
|         ); | ||||
|         $this->addSql('CREATE INDEX IDX_BCA5A36B217BBB47 ON chill_person_identifier (person_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_BCA5A36BD11EA911 ON chill_person_identifier (definition_id)'); | ||||
|         $this->addSql( | ||||
|             <<<'SQL' | ||||
|                 CREATE TABLE chill_person_identifier_definition ( | ||||
|                 id INT NOT NULL, | ||||
|                 label JSON DEFAULT '[]' NOT NULL, | ||||
|                 engine VARCHAR(100) NOT NULL, | ||||
|                 is_searchable BOOLEAN DEFAULT false NOT NULL, | ||||
|                 is_editable_by_users BOOLEAN DEFAULT false NOT NULL, | ||||
|                 data JSONB DEFAULT '[]' NOT NULL, | ||||
|                 active BOOLEAN DEFAULT true NOT NULL, | ||||
|                 PRIMARY KEY(id)) | ||||
|                 SQL | ||||
|         ); | ||||
|         $this->addSql('ALTER TABLE chill_person_identifier ADD CONSTRAINT FK_BCA5A36B217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_person_identifier ADD CONSTRAINT FK_BCA5A36BD11EA911 FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('DROP SEQUENCE chill_person_identifier_id_seq CASCADE'); | ||||
|         $this->addSql('DROP SEQUENCE chill_person_identifier_definition_id_seq CASCADE'); | ||||
|         $this->addSql('ALTER TABLE chill_person_identifier DROP CONSTRAINT FK_BCA5A36B217BBB47'); | ||||
|         $this->addSql('ALTER TABLE chill_person_identifier DROP CONSTRAINT FK_BCA5A36BD11EA911'); | ||||
|         $this->addSql('DROP TABLE chill_person_identifier'); | ||||
|         $this->addSql('DROP TABLE chill_person_identifier_definition'); | ||||
|     } | ||||
| } | ||||
| @@ -21,6 +21,9 @@ accompanying_period: | ||||
|             other {Participants} | ||||
|         } | ||||
|  | ||||
|     number: >- | ||||
|         n° {id} | ||||
|  | ||||
| person: | ||||
|     from_the: depuis le | ||||
|     And himself: >- | ||||
|   | ||||
| @@ -102,6 +102,9 @@ spokenLanguages: Langues parlées | ||||
| Employment status: Situation professionelle | ||||
| Administrative status: Situation administrative | ||||
|  | ||||
| person: | ||||
|     Identifiers: Identifiants | ||||
|  | ||||
|  | ||||
| # dédoublonnage | ||||
| Old person: Doublon | ||||
|   | ||||
		Reference in New Issue
	
	Block a user