Fixed: [export][list person] use address from household for person list

This commit is contained in:
Julien Fastré 2022-10-25 10:23:02 +02:00
parent 781253a854
commit 2096e175d4
6 changed files with 515 additions and 237 deletions

View File

@ -6,7 +6,7 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt
These are alias conventions : These are alias conventions :
| Entity | Join | Attribute | Alias | | Entity | Join | Attribute | Alias |
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:----------------------------------| |:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------|
| AccompanyingPeriod::class | | | acp | | AccompanyingPeriod::class | | | acp |
| | AccompanyingPeriodWork::class | acp.works | acpw | | | AccompanyingPeriodWork::class | acp.works | acpw |
| | AccompanyingPeriodParticipation::class | acp.participations | acppart | | | AccompanyingPeriodParticipation::class | acp.participations | acppart |
@ -36,6 +36,7 @@ These are alias conventions :
| | MaritalStatus::class | person.maritalStatus | personmarital | | | MaritalStatus::class | person.maritalStatus | personmarital |
| | VendeePerson::class | | vp | | | VendeePerson::class | | vp |
| | VendeePersonMineur::class | | vpm | | | VendeePersonMineur::class | | vpm |
| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) |
| ResidentialAddress::class | | | resaddr | | ResidentialAddress::class | | | resaddr |
| | ThirdParty::class | resaddr.hostThirdParty | tparty | | | ThirdParty::class | resaddr.hostThirdParty | tparty |
| ThirdParty::class | | | tparty | | ThirdParty::class | | | tparty |

View File

@ -0,0 +1,247 @@
<?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\MainBundle\Export\Helper;
use Chill\MainBundle\Repository\AddressRepository;
use Chill\MainBundle\Templating\Entity\AddressRender;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use LogicException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use function strlen;
/**
* Helps to load addresses and format them in list
*/
class ExportAddressHelper
{
public const ATTRIBUTES = 0b01000000;
public const BUILDING = 0b00001000;
public const COUNTRY = 0b00000001;
public const GEOM = 0b00100000;
public const POSTAL_CODE = 0b00000010;
public const STREET = 0b00000100;
public const STRING = 0b00010000;
private const ALL = [
'country' => self::COUNTRY,
'postal_code' => self::POSTAL_CODE,
'street' => self::STREET,
'building' => self::BUILDING,
'string' => self::STRING,
'geom' => self::GEOM,
'attributes' => self::ATTRIBUTES,
];
private const COLUMN_MAPPING = [
'country' => ['country'],
'postal_code' => ['postcode_code', 'postcode_name'],
'street' => ['street', 'streetNumber'],
'building' => ['buildingName', 'corridor', 'distribution', 'extra', 'flat', 'floor'],
'string' => ['_as_string'],
'attributes' => ['isNoAddress', 'confidential', 'id'],
'geom' => ['_lat', '_lon'],
];
private AddressRender $addressRender;
private AddressRepository $addressRepository;
private PropertyAccessor $propertyAccess;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
AddressRepository $addressRepository,
TranslatableStringHelperInterface $translatableStringHelper,
AddressRender $addressRender
) {
$this->addressRepository = $addressRepository;
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
$this->translatableStringHelper = $translatableStringHelper;
$this->addressRender = $addressRender;
}
/**
* @return array|string[]
*/
public function getKeys(int $params, string $prefix = ''): array
{
$prefixes = [];
foreach (self::ALL as $key => $bitmask) {
if (($params & $bitmask) === $bitmask) {
$prefixes = array_merge(
$prefixes,
array_map(
static function ($item) use ($prefix) { return $prefix . $item; },
self::COLUMN_MAPPING[$key]
)
);
}
}
return $prefixes;
}
public function getLabel($key, array $values, $data, string $prefix = '', string $translationPrefix = 'export.address_helper.'): callable
{
$sanitizedKey = substr($key, strlen($prefix));
switch ($sanitizedKey) {
case 'id':
case 'street':
case 'streetNumber':
case 'buildingName':
case 'corridor':
case 'distribution':
case 'extra':
case 'flat':
case 'floor':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
return $this->propertyAccess->getValue($address, $sanitizedKey);
};
case '_lat':
case '_lon':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
$geom = $address->getPoint();
if (null === $geom) {
return '';
}
switch ($sanitizedKey) {
case '_lat':
return $geom->getLat();
case '_lon':
return $geom->getLon();
default:
throw new LogicException('only _lat or _lon accepted, given: ' . $sanitizedKey);
}
};
case 'isNoAddress':
case 'confidential':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
switch ($val = $this->propertyAccess->getValue($address, $sanitizedKey)) {
case null:
return '';
case true:
return 1;
case false:
return 0;
default:
throw new LogicException('this value is not supported for ' . $sanitizedKey . ': ' . $val);
}
};
case 'country':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
return $this->translatableStringHelper->localize($address->getPostcode()->getCountry()->getName());
};
case '_as_string':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
return $this->addressRender->renderString($address, []);
};
case 'postcode_code':
case 'postcode_name':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
switch ($sanitizedKey) {
case 'postcode_code':
return $address->getPostcode()->getCode();
case 'postcode_name':
return $address->getPostcode()->getName();
default:
throw new LogicException('this key is not supported: ' . $sanitizedKey);
}
};
default:
throw new LogicException('this key is not supported: ' . $sanitizedKey);
}
}
}

View File

@ -3,6 +3,9 @@ services:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Chill\MainBundle\Export\Helper\:
resource: '../../Export/Helper'
chill.main.export_element_validator: chill.main.export_element_validator:
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
tags: tags:

View File

@ -515,3 +515,22 @@ notification:
Remove an email: Supprimer l'adresse email Remove an email: Supprimer l'adresse email
Email with access link: Adresse email ayant reçu un lien d'accès Email with access link: Adresse email ayant reçu un lien d'accès
export:
address_helper:
id: Identifiant de l'adresse
street: Voie
streetNumber: Numéro de voie
buildingName: Résidence
corridor: Couloir
distribution: Distribution
extra: Extra
flat: Appartement
floor: Étage
postcode_code: Code postal
postcode_name: Libellé du code postal
country: Pays
_as_string: Adresse formattée
confidential: Adresse confidentielle ?
isNoAddress: Adresse incomplète ?
_lat: Latitude
_lon: Longitude

View File

@ -1768,7 +1768,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
} }
/** /**
* @param type $spokenLanguages * @param Collection $spokenLanguages
*/ */
public function setSpokenLanguages($spokenLanguages): self public function setSpokenLanguages($spokenLanguages): self
{ {

View File

@ -17,28 +17,33 @@ use Chill\CustomFieldsBundle\Service\CustomFieldProvider;
use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface; use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTime; use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Exception; use Exception;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function addcslashes; use function addcslashes;
use function array_key_exists; use function array_key_exists;
use function array_keys; use function array_keys;
use function array_merge; use function array_merge;
use function count; use function count;
use function in_array; use function in_array;
use function strlen;
use function strtolower; use function strtolower;
use function uniqid; use function uniqid;
@ -47,31 +52,40 @@ use function uniqid;
*/ */
class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
{ {
protected CustomFieldProvider $customFieldProvider; public const FIELDS = [
'id',
protected EntityManagerInterface $entityManager; 'firstName',
'lastName',
protected array $fields = [ 'birthdate',
'id', 'firstName', 'lastName', 'birthdate',
'placeOfBirth', 'gender', 'memo', 'email', 'phonenumber', 'placeOfBirth', 'gender', 'memo', 'email', 'phonenumber',
'mobilenumber', 'contactInfo', 'countryOfBirth', 'nationality', 'mobilenumber', 'contactInfo', 'countryOfBirth', 'nationality',
'address_street_address_1', 'address_street_address_2', 'address',
'address_valid_from', 'address_postcode_label', 'address_postcode_code',
'address_country_name', 'address_country_code', 'address_isnoaddress',
]; ];
protected TranslatableStringHelper $translatableStringHelper; private ExportAddressHelper $addressHelper;
protected TranslatorInterface $translator; private CountryRepository $countryRepository;
private CustomFieldProvider $customFieldProvider;
private EntityManagerInterface $entityManager;
private $slugs = []; private $slugs = [];
private TranslatableStringHelper $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct( public function __construct(
CountryRepository $countryRepository,
ExportAddressHelper $addressHelper,
EntityManagerInterface $em, EntityManagerInterface $em,
TranslatorInterface $translator, TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper, TranslatableStringHelper $translatableStringHelper,
CustomFieldProvider $customFieldProvider CustomFieldProvider $customFieldProvider
) { ) {
$this->addressHelper = $addressHelper;
$this->countryRepository = $countryRepository;
$this->entityManager = $em; $this->entityManager = $em;
$this->translator = $translator; $this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
@ -80,7 +94,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
public function buildForm(FormBuilderInterface $builder) public function buildForm(FormBuilderInterface $builder)
{ {
$choices = array_combine($this->fields, $this->fields); $choices = array_combine(self::FIELDS, self::FIELDS);
foreach ($this->getCustomFields() as $cf) { foreach ($this->getCustomFields() as $cf) {
$choices[$this->translatableStringHelper->localize($cf->getName())] $choices[$this->translatableStringHelper->localize($cf->getName())]
@ -96,7 +110,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
'label' => 'Fields to include in export', 'label' => 'Fields to include in export',
'choice_attr' => static function (string $val): array { 'choice_attr' => static function (string $val): array {
// add a 'data-display-target' for address fields // add a 'data-display-target' for address fields
if (substr($val, 0, 8) === 'address_') { if (substr($val, 0, 7) === 'address') {
return ['data-display-target' => 'address_date']; return ['data-display-target' => 'address_date'];
} }
@ -111,17 +125,14 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
} }
}, },
])], ])],
'data' => array_values($choices),
]); ]);
// add a date field for addresses // add a date field for addresses
$builder->add('address_date', DateType::class, [ $builder->add('address_date', ChillDateType::class, [
'label' => 'Address valid at this date', 'label' => 'Address valid at this date',
'data' => new DateTime(), 'data' => new DateTimeImmutable(),
'attr' => ['class' => 'datepicker'], 'input' => 'datetime_immutable',
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
'required' => false,
'block_name' => 'list_export_form_address_date',
]); ]);
} }
@ -142,6 +153,10 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
public function getLabels($key, array $values, $data) public function getLabels($key, array $values, $data)
{ {
if (substr($key, 0, strlen('address')) === 'address') {
return $this->addressHelper->getLabel($key, $values, $data, 'address_');
}
switch ($key) { switch ($key) {
case 'birthdate': case 'birthdate':
// for birthdate, we have to transform the string into a date // for birthdate, we have to transform the string into a date
@ -151,10 +166,11 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
return 'birthdate'; return 'birthdate';
} }
if (empty($value)) { if (null === $value) {
return ''; return '';
} }
// warning: won't work with DateTimeImmutable as we reset time a few lines later
$date = DateTime::createFromFormat('Y-m-d', $value); $date = DateTime::createFromFormat('Y-m-d', $value);
// check that the creation could occurs. // check that the creation could occurs.
if (false === $date) { if (false === $date) {
@ -162,7 +178,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
. 'not be converted to %s', $value, DateTime::class)); . 'not be converted to %s', $value, DateTime::class));
} }
return $date->format('d-m-Y'); $date->setTime(0, 0, 0);
return $date;
}; };
case 'gender': case 'gender':
@ -177,29 +195,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
case 'countryOfBirth': case 'countryOfBirth':
case 'nationality': case 'nationality':
$countryRepository = $this->entityManager
->getRepository(\Chill\MainBundle\Entity\Country::class);
// load all countries in a single query
$countryRepository->findBy(['countryCode' => $values]);
return function ($value) use ($key, $countryRepository) {
if ('_header' === $value) {
return strtolower($key);
}
if (null === $value) {
return $this->translator->trans('no data');
}
$country = $countryRepository->find($value);
return $this->translatableStringHelper->localize(
$country->getName()
);
};
case 'address_country_name':
return function ($value) use ($key) { return function ($value) use ($key) {
if ('_header' === $value) { if ('_header' === $value) {
return strtolower($key); return strtolower($key);
@ -209,25 +204,16 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
return ''; return '';
} }
return $this->translatableStringHelper->localize(json_decode($value, true)); $country = $this->countryRepository->find($value);
};
case 'address_isnoaddress': return $this->translatableStringHelper->localize(
return static function (?string $value): string { $country->getName()
if ('_header' === $value) { );
return 'address.address_homeless';
}
if (null !== $value) {
return 'X';
}
return '';
}; };
default: default:
// for fields which are associated with person // for fields which are associated with person
if (in_array($key, $this->fields, true)) { if (in_array($key, self::FIELDS, true)) {
return static function ($value) use ($key) { return static function ($value) use ($key) {
if ('_header' === $value) { if ('_header' === $value) {
return strtolower($key); return strtolower($key);
@ -246,9 +232,13 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
$fields = []; $fields = [];
foreach ($data['fields'] as $key) { foreach ($data['fields'] as $key) {
if (in_array($key, $this->fields, true)) { if (substr($key, 0, strlen('address')) === 'address') {
$fields[] = $key; $fields = array_merge($fields, $this->addressHelper->getKeys(0b01111111, 'address_'));
continue;
} }
$fields[] = $key;
} }
// add the key from slugs and return // add the key from slugs and return
@ -270,6 +260,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
return Declarations::PERSON_TYPE; return Declarations::PERSON_TYPE;
} }
/**
* @param array{fields: string[], address_date: DateTimeImmutable} $data
*/
public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{ {
$centers = array_map(static function ($el) { $centers = array_map(static function ($el) {
@ -284,8 +277,20 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
$qb = $this->entityManager->createQueryBuilder(); $qb = $this->entityManager->createQueryBuilder();
foreach ($this->fields as $f) { $qb
if (in_array($f, $data['fields'], true)) { ->from(Person::class, 'person')
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Person\PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
)
)
->setParameter('authorized_centers', $centers);
foreach (self::FIELDS as $f) {
if (!in_array($f, $data['fields'], true)) {
continue;
}
switch ($f) { switch ($f) {
case 'countryOfBirth': case 'countryOfBirth':
case 'nationality': case 'nationality':
@ -293,21 +298,31 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
break; break;
case 'address_street_address_1': case 'address':
case 'address_street_address_2': foreach ($this->addressHelper->getKeys(0b01111111, 'address_') as $key) {
case 'address_valid_from': $qb
case 'address_postcode_label': ->addSelect(sprintf('IDENTITY(currentPersonAddress.address) AS %s', $key));
case 'address_postcode_code': }
case 'address_country_name':
case 'address_country_code': if (!(in_array('currentPersonAddress', $qb->getAllAliases(), true))) {
case 'address_isnoaddress': $qb
$qb->addSelect(sprintf( ->leftJoin('person.currentPersonAddress', 'currentPersonAddress')
'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s', ->andWhere(
// get the part after address_ $qb->expr()->orX(
strtoupper(substr($f, 8)), // no address at this time
$f $qb->expr()->isNull('currentPersonAddress'),
)); // there is one address...
$qb->setParameter('address_date', $data['address_date']); $qb->expr()->andX(
$qb->expr()->lte('currentPersonAddress.validFrom', ':address_date'),
$qb->expr()->orX(
$qb->expr()->isNull('currentPersonAddress.validTo'),
$qb->expr()->gt('currentPersonAddress.validTo', ':address_date')
)
)
)
)
->setParameter('address_date', $data['address_date']);
}
break; break;
@ -315,7 +330,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
$qb->addSelect(sprintf('person.%s as %s', $f, $f)); $qb->addSelect(sprintf('person.%s as %s', $f, $f));
} }
} }
}
foreach ($this->getCustomFields() as $cf) { foreach ($this->getCustomFields() as $cf) {
$cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType());
@ -345,12 +359,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
} }
} }
$qb
->from('ChillPersonBundle:Person', 'person')
->join('person.center', 'center')
->andWhere('center IN (:authorized_centers)')
->setParameter('authorized_centers', $centers);
return $qb; return $qb;
} }
@ -368,7 +376,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
{ {
// get the field starting with address_ // get the field starting with address_
$addressFields = array_filter( $addressFields = array_filter(
$this->fields, self::FIELDS,
static fn (string $el): bool => substr($el, 0, 8) === 'address_' static fn (string $el): bool => substr($el, 0, 8) === 'address_'
); );