mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-09 00:04:59 +00:00
Add external identifiers for person, editable in edit form, with minimal features associated
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
|
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 {}
|
||||
|
@@ -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