From 2c151c2ec9ba8bdacd36c986506a5150dbd4a4b9 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Tue, 9 Aug 2022 14:30:34 +0200 Subject: [PATCH 1/7] exports: add new duration aggregator (wip) --- .../DurationAggregator.php | 99 +++++++++++++++++++ .../services/exports_accompanying_course.yaml | 7 ++ .../translations/messages.fr.yml | 1 + 3 files changed, 107 insertions(+) create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php new file mode 100644 index 000000000..edf4dc24c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php @@ -0,0 +1,99 @@ + 1 mois, + * - jusqu'à 45 jours => 1 mois, + * 15 | 45 | 75 + * --+----o----+----o----+---- + * | 30 | 60 | + * etc.) + */ +class DurationAggregator implements AggregatorInterface +{ + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + switch ($value) { + + case '_header': + return 'Duration'; + + + default: + throw new \LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['duration_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by duration'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->addSelect('(acp.closingDate - acp.openingDate) AS duration_aggregator') + ; + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('duration_aggregator'); + } else { + $qb->groupBy('duration_aggregator'); + } + + $qb->orderBy('duration_aggregator'); + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index 155ad11df..2b578b4f9 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -241,3 +241,10 @@ services: tags: - { name: chill.export_aggregator, alias: accompanyingcourse_referrer_aggregator } + chill.person.export.aggregator_duration: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\DurationAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_duration_aggregator } + diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 9bb2c7e3f..fdddd5c6c 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -569,6 +569,7 @@ Group by composition: Grouper les ménages par composition familiale Group by number of children: Grouper les ménages par nombre d'enfants ## persons aggregators +Group by duration: Grouper par durée du parcours Group people by nationality: Grouper les personnes par nationalités Group by level: Grouper par niveau Group by continents: Grouper par continent From e1ec2dc25c05bdac13226faec200c3a14ff289af Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Thu, 25 Aug 2022 12:38:00 +0200 Subject: [PATCH 2/7] exports: DurationAggregator: test qb addSelect for compute date interval --- .../ChillMainBundle/Doctrine/DQL/Extract.php | 13 +++-- .../DurationAggregator.php | 51 ++++++++++++++++--- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php index 44d20724b..72ca2b461 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php @@ -2,7 +2,9 @@ namespace Chill\MainBundle\Doctrine\DQL; +use Doctrine\ORM\Query\AST\Functions\DateDiffFunction; use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\PathExpression; use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; @@ -12,13 +14,17 @@ use Doctrine\ORM\Query\SqlWalker; * Extract postgresql function * https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT * - * Usage : EXTRACT(field FROM value) + * Usage : EXTRACT(field FROM timestamp) + * TODO allow interval usage -> EXTRACT(field FROM interval) */ class Extract extends FunctionNode { private string $field; - private PathExpression $value; + private $value; + //private PathExpression $value; + //private FunctionNode $value; + //private DateDiffFunction $value; public function getSql(SqlWalker $sqlWalker) { @@ -39,7 +45,8 @@ class Extract extends FunctionNode $parser->match(Lexer::T_FROM); - $this->value = $parser->ScalarExpression(); + //$this->value = $parser->ScalarExpression(); + $this->value = $parser->ArithmeticPrimary(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php index edf4dc24c..163c318c5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php @@ -3,8 +3,12 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Export\AggregatorInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Declarations; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; /** @@ -24,16 +28,17 @@ class DurationAggregator implements AggregatorInterface */ public function getLabels($key, array $values, $data) { - return function ($value): string { - switch ($value) { + dump($key, $values, $data); - case '_header': - return 'Duration'; - - - default: - throw new \LogicException(sprintf('The value %s is not valid', $value)); + return function ($value) use ($data): string { + if ($value === '_header') { + return 'Duration'; } + if ($value === null) { + return 'current'; // when closingDate is null + } + return $value; + //sprintf("%02d", $value) . ' days'; }; } @@ -75,7 +80,37 @@ class DurationAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { $qb + // OUI ->addSelect('(acp.closingDate - acp.openingDate) AS duration_aggregator') + //->addSelect('DATE_DIFF(acp.closingDate, acp.openingDate) AS duration_aggregator') + //->addSelect('EXTRACT(month FROM acp.openingDate) AS duration_aggregator') + //->addSelect("DATE_SUB(acp.openingDate, 6, 'day') AS duration_aggregator") + + // TODO adapter la fonction extract pour l'utiliser avec des intervals: extract(month from interval) + // et ajouter une fonction custom qui calcule les intervals, comme doctrineum/date-interval + // https://packagist.org/packages/doctrineum/date-interval#3.1.0 + // (composer fait un conflit de dépendance) + + //->addSelect(" + // EXTRACT( + // month FROM + // DATE_INTERVAL(acp.closingDate, acp.openingDate) + // ) + // AS duration_aggregator") + + // NON + //->addSelect("BETWEEN acp.openingDate AND acp.closingDate AS duration_aggregator") + //->addSelect("EXTRACT(month FROM DATE_SUB(acp.openingDate, 6, 'day')) AS duration_aggregator") + //->addSelect('EXTRACT(month FROM DATE_DIFF(acp.closingDate, acp.openingDate)) AS duration_aggregator') + /* + ->addSelect(' + ( CASE + WHEN EXTRACT(day FROM DATE_DIFF(acp.closingDate, acp.openingDate)) > 15 + THEN EXTRACT(month FROM DATE_DIFF(acp.closingDate, acp.openingDate)) +1 + ELSE EXTRACT(month FROM DATE_DIFF(acp.closingDate, acp.openingDate)) + END ) AS duration_aggregator + ') + */ ; $groupBy = $qb->getDQLPart('groupBy'); From f5baa2c152ff684bdeeabc4e25d64c5663a09b42 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Thu, 25 Aug 2022 13:06:10 +0200 Subject: [PATCH 3/7] exports: DurationAggregator with approximative(*) date interval (*) for more precise, we need dql interval function --- .../DurationAggregator.php | 28 ++++++++++++++----- .../translations/messages.fr.yml | 4 +++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php index 163c318c5..2a0215e82 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php @@ -10,6 +10,7 @@ use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Les regroupements seront un nombre de mois, arrondi à l'unité la plus proche (donc @@ -22,23 +23,33 @@ use Symfony\Component\Form\FormBuilderInterface; */ class DurationAggregator implements AggregatorInterface { + private TranslatorInterface $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } /** * @inheritDoc */ public function getLabels($key, array $values, $data) { - dump($key, $values, $data); - return function ($value) use ($data): string { + if ($value === '_header') { - return 'Duration'; + return $this->translator->trans('Rounded month duration'); } + if ($value === null) { - return 'current'; // when closingDate is null + return $this->translator->trans('current duration'); // when closingDate is null } - return $value; - //sprintf("%02d", $value) . ' days'; + + if ($value === 0) { + return $this->translator->trans("duration 0 month"); + } + + return ''. $value . $this->translator->trans(' months'); }; } @@ -81,7 +92,10 @@ class DurationAggregator implements AggregatorInterface { $qb // OUI - ->addSelect('(acp.closingDate - acp.openingDate) AS duration_aggregator') + ->addSelect(' + (acp.closingDate - acp.openingDate +15) *12/365 + AS duration_aggregator' + ) //->addSelect('DATE_DIFF(acp.closingDate, acp.openingDate) AS duration_aggregator') //->addSelect('EXTRACT(month FROM acp.openingDate) AS duration_aggregator') //->addSelect("DATE_SUB(acp.openingDate, 6, 'day') AS duration_aggregator") diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index fdddd5c6c..e105dd45f 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -570,6 +570,10 @@ Group by number of children: Grouper les ménages par nombre d'enfants ## persons aggregators Group by duration: Grouper par durée du parcours +Rounded month duration: Durée en mois (arrondie) +current duration: en cours +duration 0 month: 0 mois (<15 jours) +' months': ' mois' Group people by nationality: Grouper les personnes par nationalités Group by level: Grouper par niveau Group by continents: Grouper par continent From b5139ec460aa5567e634e7d360326fabbe99308e Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Thu, 25 Aug 2022 14:10:06 +0200 Subject: [PATCH 4/7] exports: improve activity DateAggregator - put orderby at the end - addSelect with TO_CHAR() function (cancel EXRACT()) - uniformize addSelect cases - orderBy with variable - improve getLabels --- .../ACPAggregators/DateAggregator.php | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php index 6dda268e2..1c2f02d3b 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php @@ -34,31 +34,26 @@ class DateAggregator implements AggregatorInterface public function getLabels($key, array $values, $data) { return function ($value) use ($data): string { + if ($value === '_header') { + return 'by '. $data['frequency']; + } switch ($data['frequency']) { case 'month': - if ($value === '_header') { - return 'by month'; - } $month = \DateTime::createFromFormat('!m', $value); - return - sprintf("%02d", $value) .'/'. - $month->format('F') // TODO translation ?!? - ; + return sprintf( + "%02d (%s)", + $value, + $month->format('M') + ); case 'week': - if ($value === '_header') { - return 'by week'; - } - return $this->translator->trans('for week') .' '. $value ; + //return $this->translator->trans('for week') .' '. $value ; case 'year': - if ($value === '_header') { - return 'by year'; - } - return $this->translator->trans('in year') .' '. $value ; + //return $this->translator->trans('in year') .' '. $value ; default: - throw new RuntimeException(sprintf('The value %s is not valid', $value)); + return $value; } }; } @@ -91,34 +86,24 @@ class DateAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { + $order = null; + switch ($data['frequency']) { case 'month': - $qb - //->addSelect("TO_CHAR(activity.date,'Mon') AS MON") - ->addSelect('EXTRACT(month FROM activity.date) AS date_aggregator') - //->orderBy('date_aggregator') - ; - break; + $fmt = 'MM'; break; case 'week': - $qb - ->addSelect("TO_CHAR(activity.date, 'IW') AS date_aggregator") - //->orderBy('date_aggregator') - ; - break; + $fmt = 'IW'; break; case 'year': - $qb - //->addSelect("TO_CHAR(activity.date, 'YYYY') AS date_aggregator") - ->addSelect('EXTRACT(year FROM activity.date) AS date_aggregator') - //->orderBy('date_aggregator', 'ASC') - ; - break; + $fmt = 'YYYY'; $order = 'DESC'; break; default: throw new RuntimeException(sprintf("The frequency data '%s' is invalid.", $data['frequency'])); } + $qb->addSelect(sprintf("TO_CHAR(activity.date, '%s') AS date_aggregator", $fmt)); + $groupBy = $qb->getDQLPart('groupBy'); if (!empty($groupBy)) { @@ -126,6 +111,14 @@ class DateAggregator implements AggregatorInterface } else { $qb->groupBy('date_aggregator'); } + + $orderBy = $qb->getDQLPart('orderBy'); + + if (!empty($orderBy)) { + $qb->addOrderBy('date_aggregator', $order); + } else { + $qb->orderBy('date_aggregator', $order); + } } public function applyOn(): string From f99f6d5240e4cea9dba5cfd77467e8103c3613d4 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 29 Aug 2022 15:21:05 +0200 Subject: [PATCH 5/7] export: add a geographical unit entity --- .../Entity/GeographicalUnit.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Entity/GeographicalUnit.php diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit.php new file mode 100644 index 000000000..9e119e30d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit.php @@ -0,0 +1,72 @@ +id; + } + + public function getLayerName(): ?string + { + return $this->layerName; + } + + public function getUnitName(): ?string + { + return $this->unitName; + } + + public function setLayerName(?string $layerName): self + { + $this->layerName = $layerName; + + return $this; + } + + public function setUnitName(?string $unitName): self + { + $this->unitName = $unitName; + + return $this; + } +} From 81f04d01843b34964b51c16f4e359ef0eed3edd1 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 29 Aug 2022 15:34:33 +0200 Subject: [PATCH 6/7] export: add a geographical unit entity (migration file) --- .../migrations/Version20220829132409.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220829132409.php diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220829132409.php b/src/Bundle/ChillMainBundle/migrations/Version20220829132409.php new file mode 100644 index 000000000..0bb09ef74 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220829132409.php @@ -0,0 +1,31 @@ +addSql('CREATE SEQUENCE chill_main_geographical_unit_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_geographical_unit (id INT NOT NULL, geom TEXT DEFAULT NULL, layerName VARCHAR(255) DEFAULT NULL, unitName VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_geographical_unit_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_geographical_unit'); + } +} From bf44b6b90decff0cb66aa08e850dc674d6dec0ed Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 29 Aug 2022 16:22:36 +0200 Subject: [PATCH 7/7] export: geographical unit filter --- .../GeographicalUnitStatFilter.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php index 8a18eb2fe..5445e20a4 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php @@ -2,13 +2,14 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; +use Chill\MainBundle\Entity\GeographicalUnit; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\PersonBundle\Export\Declarations; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; /** @@ -23,12 +24,6 @@ use Symfony\Component\Form\FormBuilderInterface; */ class GeographicalUnitStatFilter implements FilterInterface { - - private const LOCTYPE = [ - 'center' => 'center', - // TODO not yet implemented: https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/626 - ]; - /** * @inheritDoc */ @@ -38,9 +33,12 @@ class GeographicalUnitStatFilter implements FilterInterface ->add('date', ChillDateType::class, [ 'data' => new \DateTime(), ]) - ->add('accepted_loctype', ChoiceType::class, [ - 'choices' => self::LOCTYPE, - 'multiple' => false, + ->add('accepted_loctype', EntityType::class, [ + 'class' => GeographicalUnit::class, + 'choice_label' => function (GeographicalUnit $u) { + return $u->getUnitName(); + }, + 'multiple' => true, 'expanded' => true, ]) ;