Add external identifiers for person, editable in edit form, with minimal features associated

This commit is contained in:
2025-09-01 08:05:11 +00:00
parent 76433e2512
commit ea06a96f91
40 changed files with 1274 additions and 128 deletions

View 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

3
.gitignore vendored
View File

@@ -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

View File

@@ -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]);
}
}

View File

@@ -127,3 +127,7 @@ services:
factory: ["@doctrine", getRepository]
arguments:
- "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option"
Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository:
autowire: true
autoconfigure: true

View File

@@ -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: ')'; }
}
}

View File

@@ -44,8 +44,6 @@ section.chill-entity {
margin-left: 0.5em;
}
span.id-number {
&:before { content: '(n°'; }
&:after { content: ')'; }
}
}
p.moreinfo {}

View File

@@ -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');
}
}

View File

@@ -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;

View File

@@ -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,
]));
}
}

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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()));
}
}
}

View 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);
}
}

View File

@@ -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';
}

View File

@@ -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}");
}
}

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View File

@@ -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'] ?? '';
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}

View File

@@ -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)
),
];
}
}

View File

@@ -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;
}
}

View File

@@ -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]);
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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">&nbsp;{{ '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 -#}

View File

@@ -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">

View File

@@ -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 }}&nbsp;:</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 }}&nbsp;:</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 }}&nbsp;:</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 }}&nbsp;:</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 }}&nbsp;:</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>

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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'

View File

@@ -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');
}
}

View File

@@ -21,6 +21,9 @@ accompanying_period:
other {Participants}
}
number: >-
n° {id}
person:
from_the: depuis le
And himself: >-

View File

@@ -102,6 +102,9 @@ spokenLanguages: Langues parlées
Employment status: Situation professionelle
Administrative status: Situation administrative
person:
Identifiers: Identifiants
# dédoublonnage
Old person: Doublon