From ad72192e2431bf7abeeceb7c8e824b11bf124b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 8 Jun 2023 11:24:01 +0200 Subject: [PATCH 01/87] doc: for database printciples --- .changes/unreleased/DX-20230608-112417.yaml | 5 + .../development/database-principles.rst | 84 ++++++++++ .../development/database/table_list.csv | 155 ++++++++++++++++++ docs/source/development/index.rst | 1 + 4 files changed, 245 insertions(+) create mode 100644 .changes/unreleased/DX-20230608-112417.yaml create mode 100644 docs/source/development/database-principles.rst create mode 100644 docs/source/development/database/table_list.csv diff --git a/.changes/unreleased/DX-20230608-112417.yaml b/.changes/unreleased/DX-20230608-112417.yaml new file mode 100644 index 000000000..7091808ec --- /dev/null +++ b/.changes/unreleased/DX-20230608-112417.yaml @@ -0,0 +1,5 @@ +kind: DX +body: Documentation for database principles +time: 2023-06-08T11:24:17.701892874+02:00 +custom: + Issue: "" diff --git a/docs/source/development/database-principles.rst b/docs/source/development/database-principles.rst new file mode 100644 index 000000000..0b4a33f04 --- /dev/null +++ b/docs/source/development/database-principles.rst @@ -0,0 +1,84 @@ + +.. database-principles: + +Principes de la base de données +############################### + +Cette page donne une compréhension globale de la base de donnée de Chill, et explique quelques détails d'implémentations qui permettent d'accélérer les traitements à partir de la base de donnée, ou de l'exploiter plus aisément. + +Cette page est rédigée en français. + +.. note:: + + La stabilité du schéma de la base de donnée n'est pas garantie. + + Toutefois, ce dernier évolue relativement peu. Il est rare que des tables ou des colonnes soient supprimées ou renommées. Mais il n'est pas garanti que cela puisse arriver. + +Généralités +=========== + +Une liste commentée de toutes les tables :download:`est disponible au format CSV <./database/table_list.csv`. + +Schéma et conventions de nommage +-------------------------------- + +Au début de l'histoire de Chill, les schémas postgresql n'étaient pas exploités. Les données étaient stockées dans le schéma :code:`public`. + +Par la suite, des nouveaux bundles sont apparus, et les tables ont été classées dans des schémas dédiés. + +A l'heure actuelle: + +- pour les anciens bundle, ceux qui ont déjà des tables dans le schéma public, les nouvelles tables sont ajoutées à ce schéma. Elles sont préfixées par :code:`chill__`; +- pour les bundles plus récents, les tables sont créées dans le schéma dédié + +Données avec de l'historicité +----------------------------- + +Certaines données sont historisées: + +- les référents d'un parcours; +- les statuts d'un parcours; +- la liaison entre les centres et les usagers; +- etc. + +Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable. + +Dans certains cas, la donnée actuelle (référent d'un parcours, par exemple) est également répétée au niveau de la table en elle-même. Par exemple, la table des parcours :code:`chill_person_accompanying_period` comporte une colonne :code:`step` (le statut du parcours) et :code:`user_id` (id du référent) en plus de l'historique. Bien que redondant, cela simplifie les traitements. + +Relations particulières +======================= + +Usagers, ménages, adresses +-------------------------- + +Les usagers ont une adresse au travers des ménages: dans l'interface, l'adresse est inscrite dans le dossier du ménage, et elle est "donnée" aux usagers membres du ménage, **et** qui partagent l'adresse de ce ménage. En effet, il est possible que des usagers "appartiennent" à un ménage sans y être domicilié: c'est le cas, par exemple, des enfants en garde alternée. + +L'historique de l'appartenance des usagers au ménage est conservée, de même que l'historique des adresses pour un même ménage. + +Les tables en jeu sont les suivantes: + +- la table :code:`chill_person_person` liste les usagers; +- la table :code:`chill_person_household_members` liste les appartenances au ménage: il s'agit de la jointure entre les usagers et les ménages: + - les colonnes :code:`startDate` et :code:`endDate` indiquent la date de début et la date de fin de l'appartenance; + - la colonne :code:`shareHousehold` indique si l'utilisateur partage l'adresse du ménage (si oui, sa valeur est :code:`TRUE`) +- la table :code:`chill_person_household` liste les ménages +- la table :code:`chill_person_household_to_addresses` associe les ménages aux adresses; +- la table :code:`chill_main_address` contient les adresses, en indiquant la date de début de validité (:code:`validFrom`) et la fin de validité (:code:`validTo`). + +Pour simplifier la résolution des adresses et des usagers, deux vues ont été mises en œuvre: + +- la vue :code:`view_chill_person_household_address` reprend, pour chaque usager, l'historique des appartenances au ménage découpée par l'historique des adresses d'un ménage. + Autrement dit, une ligne est créée à chaque fois qu'un usager change de ménage, ou qu'un ménage change d'adresse. Il est donc possible de retrouver l'historique complet des adresses pour un usager donné via cette table. +- la vue :code:`view_chill_person_current_address` reprend l'adresse actuelle des usagers. + +Adresses et unités géographiques +-------------------------------- + +Chill propose des statistiques sur la localisation des adresses par rapport à des zones géographiques (:code:`chill_main_geographical_unit`). + +Comme la résolution géographique des adresses est coûteuse en CPU et en temps de traitement, une vue matérialisée a été créée: :code:`view_chill_main_address_geographical_unit`. Elle est rafraichie quotidiennement dans la base de donnée de production. + +Liste des tables et commentaires +================================ + + diff --git a/docs/source/development/database/table_list.csv b/docs/source/development/database/table_list.csv new file mode 100644 index 000000000..fe688318d --- /dev/null +++ b/docs/source/development/database/table_list.csv @@ -0,0 +1,155 @@ +order,table_schema,table_name,commentaire +1,chill_3party,party_category,Catégorie de tiers +2,chill_3party,party_center,Association entre les tiers et les centres (déprécié) +3,chill_3party,party_profession,Profession du tiers (déprécié) +4,chill_3party,third_party,Tiers +5,chill_3party,thirdparty_category,association tiers - catégories +6,chill_asideactivity,asideactivity,Activités annexes +7,chill_asideactivity,asideactivitycategory,Catégories d'activités annexes +8,chill_budget,charge,Charges du budget +9,chill_budget,charge_type,Types de charges +10,chill_budget,resource,Ressources du budget +11,chill_budget,resource_type,Types de ressources +12,chill_calendar,calendar,Rendez-vous +13,chill_calendar,calendar_doc,Document du rendez-vous +14,chill_calendar,calendar_range,Plage de disponibilité +15,chill_calendar,calendar_to_persons,association rendez-vous - usagers +16,chill_calendar,calendar_to_thirdparties,association rendez-vous - tiers +17,chill_calendar,cancel_reason,Motifs d'annulations +18,chill_calendar,invite,Invitation aux rendez-vous +19,chill_doc,accompanyingcourse_document,Documents associés aux parcours +20,chill_doc,document_category,Catégories de documents +21,chill_doc,person_document,Documents associés à l'usagers +22,chill_doc,stored_object,Documents +23,chill_task,recurring_task,Tâches récurrentes (non utilisé) +24,chill_task,single_task,Tâches +25,chill_task,single_task_place_event,Historique des transitions des tâches +26,chill_vendee,adressederelais, +27,chill_vendee,center_polygon +28,chill_vendee,entourage, +29,chill_vendee,geographical_unit +30,chill_vendee,geographical_unit_association +31,chill_vendee,mobilite +32,chill_vendee,niveauetude +33,chill_vendee,security_profile +34,chill_vendee,security_profile_action +35,chill_vendee,security_profile_jobs +36,chill_vendee,situationprofessionelle +37,chill_vendee,statutlogement +38,chill_vendee,tempsdetravail +39,chill_vendee,titredesejour +40,chill_vendee,vendee_person +41,chill_vendee,vendee_person_mineur +42,chill_vendee,vendeeperson_entourage +43,chill_vendee,vendeepersonmineur_adressederelais +44,public,accompanying_periods_scopes,Services associés aux parcours +45,public,activity,Échanges +46,public,activity_activityreason,s +47,public,activity_person, +48,public,activity_storedobject, +49,public,activity_thirdparty, +50,public,activity_user, +51,public,activityreason,Sujets d'échange +52,public,activityreasoncategory,Catégories de sujets +53,public,activitytpresence,Présence aux échanges +54,public,activitytype,Types d'échanges +55,public,activitytypecategory,Catégories de types d'échanges +56,public,centers,"Centres (territoires, agences, etc.)" +57,public,chill_activity_activity_chill_person_socialaction, +58,public,chill_activity_activity_chill_person_socialissue +59,public,chill_docgen_template,Gabarits de documents +60,public,chill_main_address,Adresses +61,public,chill_main_address_legacy,Anciennes adresses (dépréciés) +62,public,chill_main_address_reference,Adresses de référence +63,public,chill_main_civility,Civilités +64,public,chill_main_cronjob_execution,Dernière exécution des tâche cron +65,public,chill_main_geographical_unit,Unités géographiques +66,public,chill_main_geographical_unit_layer,Couches d'unités géographiques +67,public,chill_main_location,Localisations +68,public,chill_main_location_type,Types de localisations +69,public,chill_main_notification,Notifications +70,public,chill_main_notification_addresses_unread +71,public,chill_main_notification_addresses_user +72,public,chill_main_notification_comment, +73,public,chill_main_postal_code,Code postaux +74,public,chill_main_saved_export,Exports enregistrés +75,public,chill_main_user_job,Métiers +76,public,chill_main_workflow_entity,Workflows +77,public,chill_main_workflow_entity_comment +78,public,chill_main_workflow_entity_step,Etapes du workflow +79,public,chill_main_workflow_entity_step_cc_user, +80,public,chill_main_workflow_entity_step_user +81,public,chill_main_workflow_entity_step_user_by_accesskey, +82,public,chill_main_workflow_entity_subscriber_to_final, +83,public,chill_main_workflow_entity_subscriber_to_step +84,public,chill_person_accompanying_period,Parcours d'accompagnement +85,public,chill_person_accompanying_period_closingmotive,Motifs de cloture des parcours +86,public,chill_person_accompanying_period_comment,Commentaires des parcours +87,public,chill_person_accompanying_period_location_history,Historique de la localisatio ndes parcours +88,public,chill_person_accompanying_period_origin,Origine des parcours +89,public,chill_person_accompanying_period_participation,Appartenance des usagers au parcours +90,public,chill_person_accompanying_period_resource,Personnes ressources d'un parcours +91,public,chill_person_accompanying_period_social_issues, +92,public,chill_person_accompanying_period_step_history +93,public,chill_person_accompanying_period_user_history +94,public,chill_person_accompanying_period_work,Actions d'accompagnements +95,public,chill_person_accompanying_period_work_evaluation,Évaluations (dans les actions d'accompagnements) +96,public,chill_person_accompanying_period_work_evaluation_document,Documents des évaluations +97,public,chill_person_accompanying_period_work_goal,Objectifs d'une actions +98,public,chill_person_accompanying_period_work_goal_result,Objectifs et résultats d'une action +99,public,chill_person_accompanying_period_work_person,Usagers associés à une actions +100,public,chill_person_accompanying_period_work_referrer,Référents d'une actions +101,public,chill_person_accompanying_period_work_result,Résultats d'une action +102,public,chill_person_accompanying_period_work_third_party,Tiers traitants d'une action +103,public,chill_person_alt_name,"Noms supplémentaires d'un usager (nom marital, etc.)" +104,public,chill_person_household,Ménages +105,public,chill_person_household_composition, +106,public,chill_person_household_composition_type,Types de composition de ménage +107,public,chill_person_household_members,Membres du ménages +108,public,chill_person_household_position,Positions dans le ménage +109,public,chill_person_household_to_addresses,Association adresses - ménages +110,public,chill_person_marital_status,Etats civils +111,public,chill_person_not_duplicate, +112,public,chill_person_person,Usagers +113,public,chill_person_person_center_history,Historique des centres d'un usagers +114,public,chill_person_persons_to_addresses,Déprécié +115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager +116,public,chill_person_relations,Types de relations de filiation +117,public,chill_person_relationships,Relations de filiations +118,public,chill_person_residential_address,Adresses de résidences +119,public,chill_person_resource,Personnes ressources (pour les personnes) +120,public,chill_person_resource_kind,Type de personnes ressources +121,public,chill_person_social_action,Liste des actions d'accompagnement +122,public,chill_person_social_action_goal,Objectifs associés à une action +123,public,chill_person_social_action_result,Résultats associés à une action +124,public,chill_person_social_issue,Problématiques sociales +125,public,chill_person_social_work_evaluation,Evaluations disponibles +126,public,chill_person_social_work_evaluation_action,Associations entre les évaluations et les actions +127,public,chill_person_social_work_goal,Objectifs disponibles pour une actions +128,public,chill_person_social_work_goal_result,Objectifs et résultats disponible pour une action +129,public,chill_person_social_work_result,Résultats disponibles pour une action +130,public,country,Pays +131,public,custom_field_long_choice_options, +132,public,customfield +133,public,customfieldsdefaultgroup +134,public,customfieldsgroup +135,public,geography_columns,Table liée à postgis +136,public,geometry_columns,Table liée à postgis +137,public,group_centers, +138,public,language,Langues +139,public,messenger_messages,Table système +140,public,migration_versions,Table système +141,public,permission_groups +142,public,permissionsgroup_rolescope +143,public,persons_spoken_languages +144,public,regroupment,Regroupement de centres +145,public,regroupment_center, +146,public,role_scopes, +147,public,scopes,Services +148,public,spatial_ref_sys,Table système (postgis) +149,public,user_groupcenter, +150,public,users,Utilisateurs +151,public,view_chill_person_accompanying_period_info, +152,public,view_chill_person_current_address +153,public,view_chill_person_household_address +154,public,view_chill_person_person_center_history_current diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index fd9ae43ba..768d29ce0 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -36,6 +36,7 @@ As Chill rely on the `symfony `_ framework, reading the fram Assets Cron Jobs Info about entities + Info about database (in French) Layout and UI ************** From 659dff3d2c310951bf3a0dce6474ed375e4d5b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 23 Jun 2023 12:19:40 +0200 Subject: [PATCH 02/87] DX: Add features to filterOrder Allow to add single checkboxes and entitychoices to filter order --- .changes/unreleased/DX-20230623-122408.yaml | 5 ++ .../Form/Type/Listing/FilterOrderType.php | 54 ++++++------- .../views/FilterOrder/base.html.twig | 50 ++++++++++-- .../Templating/Listing/FilterOrderHelper.php | 76 +++++++++++++++++-- .../Listing/FilterOrderHelperBuilder.php | 39 ++++++++++ .../Templating/Listing/Templating.php | 39 +++++++++- 6 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 .changes/unreleased/DX-20230623-122408.yaml diff --git a/.changes/unreleased/DX-20230623-122408.yaml b/.changes/unreleased/DX-20230623-122408.yaml new file mode 100644 index 000000000..58dd96180 --- /dev/null +++ b/.changes/unreleased/DX-20230623-122408.yaml @@ -0,0 +1,5 @@ +kind: DX +body: '[FilterOrderHelper] add entity choice and singleCheckbox' +time: 2023-06-23T12:24:08.133491895+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index aaa6afa24..16038515d 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Form\Type\Listing; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\SearchType; @@ -27,13 +29,6 @@ use function count; final class FilterOrderType extends \Symfony\Component\Form\AbstractType { - private RequestStack $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - } - public function buildForm(FormBuilderInterface $builder, array $options) { /** @var FilterOrderHelper $helper */ @@ -71,6 +66,25 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $builder->add($checkboxesBuilder); } + if ([] !== $helper->getEntityChoices()) { + $entityChoicesBuilder = $builder->create('entity_choices', null, ['compound' => true]); + + foreach ($helper->getEntityChoices() as $key => [ + 'label' => $label, 'choices' => $choices, 'options' => $opts, 'class' => $class + ]) { + $entityChoicesBuilder->add($key, EntityType::class, [ + 'label' => $label, + 'choices' => $choices, + 'class' => $class, + 'multiple' => true, + 'expanded' => true, + ...$opts, + ]); + } + + $builder->add($entityChoicesBuilder); + } + if (0 < count($helper->getDateRanges())) { $dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]); @@ -97,28 +111,14 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $builder->add($dateRangesBuilder); } - foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { - switch ($key) { - case 'q': - case 'checkboxes' . $key: - case $key . '_from': - case $key . '_to': - break; + if ([] !== $helper->getSingleCheckbox()) { + $singleCheckBoxBuilder = $builder->create('single_checkboxes', null, ['compound' => true]); - case 'page': - $builder->add($key, HiddenType::class, [ - 'data' => 1, - ]); - - break; - - default: - $builder->add($key, HiddenType::class, [ - 'data' => $value, - ]); - - break; + foreach ($helper->getSingleCheckbox() as $name => ['label' => $label]) { + $singleCheckBoxBuilder->add($name, CheckboxType::class, ['label' => $label, 'required' => false]); } + + $builder->add($singleCheckBoxBuilder); } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 1f1e54c72..d4a6bbdd4 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -13,13 +13,13 @@ {% if form.dateRanges is defined %} {% if form.dateRanges|length > 0 %} {% for dateRangeName, _o in form.dateRanges %} -
+
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} -
+
{{ form_label(form.dateRanges[dateRangeName])}}
{% endif %} -
+
{{ 'chill_calendar.From'|trans }} {{ form_widget(form.dateRanges[dateRangeName]['from']) }} @@ -27,7 +27,7 @@ {{ form_widget(form.dateRanges[dateRangeName]['to']) }}
-
+
@@ -37,7 +37,7 @@ {% if form.checkboxes is defined %} {% if form.checkboxes|length > 0 %} {% for checkbox_name, options in form.checkboxes %} -
+
{% for c in form['checkboxes'][checkbox_name].children %}
@@ -61,5 +61,45 @@ {% endfor %} {% endif %} {% endif %} + {% if form.entity_choices is defined %} + {% if form.entity_choices |length > 0 %} + {% for checkbox_name, options in form.entity_choices %} +
+ {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} +
+ {{ form_label(form.entity_choices[checkbox_name])}} +
+ {% endif %} +
+ {% for c in form['entity_choices'][checkbox_name].children %} +
+ {{ form_widget(c) }} + {{ form_label(c) }} +
+ {% endfor %} +
+
+ +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.single_checkboxes is defined %} + {% for name, _o in form.single_checkboxes %} +
+
+ {{ form_widget(form.single_checkboxes[name]) }} +
+
+ +
+
+ {% endfor %} + {% endif %}
+ + {% for k,v in otherParameters %} + + {% endfor %} {{ form_end(form) }} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 367cc0861..28cc8e331 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,11 +26,16 @@ class FilterOrderHelper { private array $checkboxes = []; + /** + * @var array + */ + private array $singleCheckbox = []; + private array $dateRanges = []; private FormFactoryInterface $formFactory; - private ?string $formName = 'f'; + public const FORM_NAME = 'f'; private array $formOptions = []; @@ -40,6 +47,11 @@ class FilterOrderHelper private ?array $submitted = null; + /** + * @var array + */ + private array $entityChoices = []; + public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack @@ -48,7 +60,29 @@ class FilterOrderHelper $this->requestStack = $requestStack; } - public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self + public function addSingleCheckbox(string $name, string $label): self + { + $this->singleCheckbox[$name] = ['label' => $label]; + + return $this; + } + + /** + * @param class-string $class + */ + public function addEntityChoice(string $name, string $class, string $label, array $choices, array $options = []): self + { + $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; + + return $this; + } + + public function getEntityChoices(): array + { + return $this->entityChoices; + } + + public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { $missing = count($choices) - count($trans) - 1; $this->checkboxes[$name] = [ @@ -58,6 +92,7 @@ class FilterOrderHelper 0 < $missing ? array_fill(0, $missing, null) : [] ), + ...$options, ]; return $this; @@ -73,7 +108,7 @@ class FilterOrderHelper public function buildForm(): FormInterface { return $this->formFactory - ->createNamed($this->formName, $this->formType, $this->getDefaultData(), array_merge([ + ->createNamed(self::FORM_NAME, $this->formType, $this->getDefaultData(), array_merge([ 'helper' => $this, 'method' => 'GET', 'csrf_protection' => false, @@ -86,13 +121,31 @@ class FilterOrderHelper return $this->getFormData()['checkboxes'][$name]; } + public function getSingleCheckboxData(string $name): ?bool + { + return $this->getFormData()['single_checkboxes'][$name]; + } + + public function getEntityChoiceData($name): mixed + { + return $this->getFormData()['entity_choices'][$name]; + } + public function getCheckboxes(): array { return $this->checkboxes; } /** - * @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable> + * @return array + */ + public function getSingleCheckbox(): array + { + return $this->singleCheckbox; + } + + /** + * @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable} */ public function getDateRangeData(string $name): array { @@ -123,7 +176,12 @@ class FilterOrderHelper private function getDefaultData(): array { - $r = []; + $r = [ + 'checkboxes' => [], + 'dateRanges' => [], + 'single_checkboxes' => [], + 'entity_choices' => [] + ]; if ($this->hasSearchBox()) { $r['q'] = ''; @@ -138,6 +196,14 @@ class FilterOrderHelper $r['dateRanges'][$name]['to'] = $defaults['to']; } + foreach ($this->singleCheckbox as $name => $c) { + $r['single_checkboxes'][$name] = false; + } + + foreach ($this->entityChoices as $name => $c) { + $r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null; + } + return $r; } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index d68c8c37a..e176e27c6 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -27,6 +27,16 @@ class FilterOrderHelperBuilder private ?array $searchBoxFields = null; + /** + * @var array + */ + private array $singleCheckboxes = []; + + /** + * @var array + */ + private array $entityChoices = []; + public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack @@ -35,6 +45,13 @@ class FilterOrderHelperBuilder $this->requestStack = $requestStack; } + public function addSingleCheckbox(string $name, string $label): self + { + $this->singleCheckboxes[$name] = ['label' => $label]; + + return $this; + } + public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self { $this->checkboxes[$name] = ['choices' => $choices, 'default' => $default, 'trans' => $trans]; @@ -42,6 +59,16 @@ class FilterOrderHelperBuilder return $this; } + /** + * @param class-string $class + */ + public function addEntityChoice(string $name, string $label, string $class, array $choices, ?array $options = []): self + { + $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; + + return $this; + } + public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self { $this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label]; @@ -75,6 +102,18 @@ class FilterOrderHelperBuilder $helper->addCheckbox($name, $choices, $default, $trans); } + foreach ( + $this->singleCheckboxes as $name => ['label' => $label] + ) { + $helper->addSingleCheckbox($name, $label); + } + + foreach ( + $this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options] + ) { + $helper->addEntityChoice($name, $class, $label, $choices, $options); + } + foreach ( $this->dateRanges as $name => [ 'from' => $from, diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php index bc256e24f..b91cd86e8 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php @@ -11,13 +11,23 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Symfony\Component\HttpFoundation\RequestStack; use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; class Templating extends AbstractExtension { - public function getFilters() + public function __construct( + private readonly RequestStack $requestStack, + ) { + } + + public function getFilters(): array { return [ new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [ @@ -26,16 +36,41 @@ class Templating extends AbstractExtension ]; } + /** + * @throws SyntaxError + * @throws RuntimeError + * @throws LoaderError + */ public function renderFilterOrderHelper( Environment $environment, FilterOrderHelper $helper, ?string $template = '@ChillMain/FilterOrder/base.html.twig', ?array $options = [] - ) { + ): string { + $otherParameters = []; + + foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { + switch ($key) { + case FilterOrderHelper::FORM_NAME: + break; + + case PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY: + // when filtering, go back to page 1 + $otherParameters[PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY] = 1; + + break; + default: + $otherParameters[$key] = $value; + + break; + } + } + return $environment->render($template, [ 'helper' => $helper, 'form' => $helper->buildForm()->createView(), 'options' => $options, + 'otherParameters' => $otherParameters, ]); } } From 51544cfc48418e3492d43b232081efabf5d1772e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 23 Jun 2023 12:21:13 +0200 Subject: [PATCH 03/87] DX: improve typing of a property in UserJob --- src/Bundle/ChillMainBundle/Entity/UserJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Entity/UserJob.php b/src/Bundle/ChillMainBundle/Entity/UserJob.php index ed8bc43f3..c6bfe1aa3 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserJob.php +++ b/src/Bundle/ChillMainBundle/Entity/UserJob.php @@ -37,7 +37,7 @@ class UserJob protected ?int $id = null; /** - * @var array|string[]A + * @var array * @ORM\Column(name="label", type="json") * @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Context({"is-translatable": true}, groups={"docgen:read"}) From f7c11d356737351d7fda548b3bb5d0acc7b0fac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 23 Jun 2023 12:22:24 +0200 Subject: [PATCH 04/87] Feature: Add filters on activity list --- .../unreleased/Feature-20230623-122530.yaml | 5 + .../unreleased/Feature-20230623-122702.yaml | 6 + .../Controller/ActivityController.php | 138 ++++---- .../Repository/ActivityACLAwareRepository.php | 240 ++++++++++--- .../ActivityACLAwareRepositoryInterface.php | 36 +- .../Repository/ActivityTypeRepository.php | 4 + .../ActivityTypeRepositoryInterface.php | 4 +- .../Resources/views/Activity/list.html.twig | 3 + .../ActivityACLAwareRepositoryTest.php | 325 ++++++++++++++++++ .../translations/messages.fr.yml | 8 + 10 files changed, 648 insertions(+), 121 deletions(-) create mode 100644 .changes/unreleased/Feature-20230623-122530.yaml create mode 100644 .changes/unreleased/Feature-20230623-122702.yaml create mode 100644 src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php diff --git a/.changes/unreleased/Feature-20230623-122530.yaml b/.changes/unreleased/Feature-20230623-122530.yaml new file mode 100644 index 000000000..922750ea8 --- /dev/null +++ b/.changes/unreleased/Feature-20230623-122530.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[activity list] add filtering for activities list' +time: 2023-06-23T12:25:30.49643551+02:00 +custom: + Issue: "" diff --git a/.changes/unreleased/Feature-20230623-122702.yaml b/.changes/unreleased/Feature-20230623-122702.yaml new file mode 100644 index 000000000..e1d1b0e1f --- /dev/null +++ b/.changes/unreleased/Feature-20230623-122702.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: '[activity list] in person context, show also the activities from the accompanying + periods where the person participates' +time: 2023-06-23T12:27:02.159041095+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 444c663fc..07e4fcf62 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -18,11 +18,16 @@ use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository; use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; +use Chill\ActivityBundle\Repository\ActivityUserJobRepository; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; +use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Repository\LocationRepository; use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Privacy\PrivacyEvent; @@ -47,68 +52,25 @@ use function array_key_exists; final class ActivityController extends AbstractController { - private AccompanyingPeriodRepository $accompanyingPeriodRepository; - - private ActivityACLAwareRepositoryInterface $activityACLAwareRepository; - - private ActivityRepository $activityRepository; - - private ActivityTypeCategoryRepository $activityTypeCategoryRepository; - - private ActivityTypeRepositoryInterface $activityTypeRepository; - - private CenterResolverManagerInterface $centerResolver; - - private EntityManagerInterface $entityManager; - - private EventDispatcherInterface $eventDispatcher; - - private LocationRepository $locationRepository; - - private LoggerInterface $logger; - - private PersonRepository $personRepository; - - private SerializerInterface $serializer; - - private ThirdPartyRepository $thirdPartyRepository; - - private TranslatorInterface $translator; - - private UserRepositoryInterface $userRepository; - public function __construct( - ActivityACLAwareRepositoryInterface $activityACLAwareRepository, - ActivityTypeRepositoryInterface $activityTypeRepository, - ActivityTypeCategoryRepository $activityTypeCategoryRepository, - PersonRepository $personRepository, - ThirdPartyRepository $thirdPartyRepository, - LocationRepository $locationRepository, - ActivityRepository $activityRepository, - AccompanyingPeriodRepository $accompanyingPeriodRepository, - EntityManagerInterface $entityManager, - EventDispatcherInterface $eventDispatcher, - LoggerInterface $logger, - SerializerInterface $serializer, - UserRepositoryInterface $userRepository, - CenterResolverManagerInterface $centerResolver, - TranslatorInterface $translator + private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository, + private readonly ActivityTypeRepositoryInterface $activityTypeRepository, + private readonly ActivityTypeCategoryRepository $activityTypeCategoryRepository, + private readonly PersonRepository $personRepository, + private readonly ThirdPartyRepository $thirdPartyRepository, + private readonly LocationRepository $locationRepository, + private readonly ActivityRepository $activityRepository, + private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository, + private readonly EntityManagerInterface $entityManager, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly LoggerInterface $logger, + private readonly SerializerInterface $serializer, + private readonly UserRepositoryInterface $userRepository, + private readonly CenterResolverManagerInterface $centerResolver, + private readonly TranslatorInterface $translator, + private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, + private readonly TranslatableStringHelperInterface $translatableStringHelper, ) { - $this->activityACLAwareRepository = $activityACLAwareRepository; - $this->activityTypeRepository = $activityTypeRepository; - $this->activityTypeCategoryRepository = $activityTypeCategoryRepository; - $this->personRepository = $personRepository; - $this->thirdPartyRepository = $thirdPartyRepository; - $this->locationRepository = $locationRepository; - $this->activityRepository = $activityRepository; - $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; - $this->entityManager = $entityManager; - $this->eventDispatcher = $eventDispatcher; - $this->logger = $logger; - $this->serializer = $serializer; - $this->userRepository = $userRepository; - $this->centerResolver = $centerResolver; - $this->translator = $translator; } /** @@ -292,11 +254,27 @@ final class ActivityController extends AbstractController // TODO: add pagination [$person, $accompanyingPeriod] = $this->getEntity($request); + $filter = $this->buildFilterOrder($person ?? $accompanyingPeriod); + + $filterArgs = [ + 'my_activities' => $filter->getSingleCheckboxData('my_activities'), + 'types' => $filter->getEntityChoiceData('activity_types'), + 'jobs' => $filter->getEntityChoiceData('jobs'), + 'before' => $filter->getDateRangeData('activity_date')['to'], + 'after' => $filter->getDateRangeData('activity_date')['from'], + ]; if ($person instanceof Person) { $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person); $activities = $this->activityACLAwareRepository - ->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); + ->findByPerson( + $person, + ActivityVoter::SEE, + 0, + null, + ['date' => 'DESC', 'id' => 'DESC'], + $filterArgs + ); $event = new PrivacyEvent($person, [ 'element_class' => Activity::class, @@ -309,7 +287,14 @@ final class ActivityController extends AbstractController $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); $activities = $this->activityACLAwareRepository - ->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); + ->findByAccompanyingPeriod( + $accompanyingPeriod, + ActivityVoter::SEE, + 0, + null, + ['date' => 'DESC', 'id' => 'DESC'], + $filterArgs + ); $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; } @@ -320,10 +305,39 @@ final class ActivityController extends AbstractController 'activities' => $activities, 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, + 'filter' => $filter, ] ); } + private function buildFilterOrder(AccompanyingPeriod|Person $associated): FilterOrderHelper + { + + $filterBuilder = $this->filterOrderHelperFactory->create(self::class); + $types = $this->activityACLAwareRepository->findActivityTypeByAssociated($associated); + $jobs = $this->activityACLAwareRepository->findUserJobByAssociated($associated); + + $filterBuilder + ->addDateRange('activity_date', 'activity.date') + ->addSingleCheckbox('my_activities', 'activity_filter.My activities') + ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ + 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { + $text = match ($activityType->hasCategory()) { + true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', + false => '', + }; + + return $text . $this->translatableStringHelper->localize($activityType->getName()); + } + ]) + ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ + 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) + ]) + ; + + return $filterBuilder->build(); + } + public function newAction(Request $request): Response { $view = null; diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 1f2039a2c..1fd0a57d6 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -18,67 +18,159 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\LocationType; use Chill\MainBundle\Entity\Scope; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; +use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\ResultSetMappingBuilder; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Role\Role; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Security; use function count; use function in_array; -final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface +final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface { - private AuthorizationHelper $authorizationHelper; - - private CenterResolverDispatcherInterface $centerResolverDispatcher; - - private EntityManagerInterface $em; - - private ActivityRepository $repository; - - private Security $security; - - private TokenStorageInterface $tokenStorage; - public function __construct( - AuthorizationHelper $authorizationHelper, - CenterResolverDispatcherInterface $centerResolverDispatcher, - TokenStorageInterface $tokenStorage, - ActivityRepository $repository, - EntityManagerInterface $em, - Security $security + private AuthorizationHelperForCurrentUserInterface $authorizationHelper, + private CenterResolverManagerInterface $centerResolverManager, + private ActivityRepository $repository, + private EntityManagerInterface $em, + private Security $security, + private RequestStack $requestStack, ) { - $this->authorizationHelper = $authorizationHelper; - $this->centerResolverDispatcher = $centerResolverDispatcher; - $this->tokenStorage = $tokenStorage; - $this->repository = $repository; - $this->em = $em; - $this->security = $security; } - public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array + public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array { - $user = $this->security->getUser(); - $center = $this->centerResolverDispatcher->resolveCenter($period); + $qb = $this->buildBaseQuery($filters); - if (0 === count($orderBy)) { - $orderBy = ['date' => 'DESC']; + $qb->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); + + foreach ($orderBy as $field => $order) { + $qb->addOrderBy('a.' . $field, $order); } - $scopes = $this->authorizationHelper - ->getReachableCircles($user, $role, $center); + $qb->setFirstResult(0)->setMaxResults(1000); - return $this->em->getRepository(Activity::class) - ->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy); + return $qb->getQuery()->getResult(); } + public function buildBaseQuery(array $filters): QueryBuilder + { + $qb = $this->repository + ->createQueryBuilder('a') + ; + + if (($filters['my_activities'] ?? false) and ($user = $this->security->getUser()) instanceof User) { + $qb->andWhere( + $qb->expr()->orX( + 'a.createdBy = :user', + 'a.user = :user', + ':user MEMBER OF a.users' + ) + )->setParameter('user', $user); + } + + if ([] !== ($types = $filters['types'] ?? [])) { + $qb->andWhere('a.activityType IN (:types)')->setParameter('types', $types); + } + + if ([] !== ($jobs = $filters['jobs'] ?? [])) { + $qb + ->leftJoin('a.createdBy', 'creator') + ->leftJoin('a.user', 'activity_u') + ->andWhere( + $qb->expr()->orX( + 'creator.userJob IN (:jobs)', + 'activity_u.userJob IN (:jobs)', + 'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))' + ) + ) + ->setParameter('jobs', $jobs); + } + + if (null !== ($after = $filters['after'] ?? null)) { + $qb->andWhere('a.date >= :after')->setParameter('after', $after); + } + + if (null !== ($before = $filters['before'] ?? null)) { + $qb->andWhere('a.date <= :before')->setParameter('before', $before); + } + + return $qb; + } + + /** + * @param AccompanyingPeriod|Person $associated + * @return array + */ + public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array + { + $in = $this->em->createQueryBuilder(); + $in + ->select('1') + ->from(Activity::class, 'a'); + + if ($associated instanceof Person) { + $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); + } else { + $in->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $associated); + } + + // join between the embedded exist query and the main query + $in->andWhere('a.activityType = t'); + + $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); + $qb + ->select('t') + ->from(ActivityType::class, 't') + ->where( + $qb->expr()->exists($in->getDQL()) + ); + + return $qb->getQuery()->getResult(); + } + + public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array + { + $in = $this->em->createQueryBuilder(); + $in->select('IDENTITY(u.userJob)') + ->from(User::class, 'u') + ->join( + Activity::class, + 'a', + Join::WITH, + 'a.createdBy = u OR a.user = u OR u MEMBER OF a.users' + ); + + if ($associated instanceof Person) { + $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); + } else { + $in->andWhere('a.accompanyingPeriod = :associated'); + $in->setParameter('associated', $associated); + } + + $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); + + $qb->select('ub', 'JSON_EXTRACT(ub.label, :lang) AS HIDDEN lang') + ->from(UserJob::class, 'ub') + ->where($qb->expr()->in('ub.id', $in->getDQL())) + ->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale()) + ->orderBy('lang') + ; + + return $qb->getQuery()->getResult(); + } + + public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array { $rsm = new ResultSetMappingBuilder($this->em); @@ -159,25 +251,66 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte return $nq->getResult(AbstractQuery::HYDRATE_ARRAY); } - /** - * @param array $orderBy - * - * @return Activity[]|array - */ - public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array + public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): array { - $user = $this->security->getUser(); - $center = $this->centerResolverDispatcher->resolveCenter($person); + $qb = $this->buildBaseQuery($filters); - if (0 === count($orderBy)) { - $orderBy = ['date' => 'DESC']; + $qb = $this->filterBaseQueryByPerson($qb, $person, $role); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('a.' . $field, $direction); } - $reachableScopes = $this->authorizationHelper - ->getReachableCircles($user, $role, $center); + return $qb->getQuery()->getResult(); + } - return $this->em->getRepository(Activity::class) - ->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start); + private function filterBaseQueryByPerson(QueryBuilder $qb, Person $person, string $role): QueryBuilder + { + $orX = $qb->expr()->orX(); + $counter = 0; + foreach ($this->centerResolverManager->resolveCenters($person) as $center) { + $scopes = $this->authorizationHelper->getReachableScopes($role, $center); + + if ([] === $scopes) { + continue; + } + + $orX->add(sprintf('a.person = :person AND a.scope IN (:scopes_%d)', $counter)); + $qb->setParameter(sprintf('scopes_%d', $counter), $scopes); + $qb->setParameter('person', $person); + $counter++; + } + + foreach ($person->getAccompanyingPeriodParticipations() as $participation) { + if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { + continue; + } + + $and = $qb->expr()->andX( + sprintf('a.accompanyingPeriod = :period_%d', $counter), + sprintf('a.date >= :participation_start_%d', $counter) + ); + + $qb + ->setParameter(sprintf('period_%d', $counter), $participation->getAccompanyingPeriod()) + ->setParameter(sprintf('participation_start_%d', $counter), $participation->getStartDate()); + + if (null !== $participation->getEndDate()) { + $and->add(sprintf('a.date < :participation_end_%d', $counter)); + $qb + ->setParameter(sprintf('participation_end_%d', $counter), $participation->getEndDate()); + } + $orX->add($and); + $counter++; + } + + if (0 === $orX->count()) { + $qb->andWhere('FALSE = TRUE'); + } else { + $qb->andWhere($orX); + } + + return $qb; } public function queryTimelineIndexer(string $context, array $args = []): array @@ -226,7 +359,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte // acls: $reachableCenters = $this->authorizationHelper->getReachableCenters( - $this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE ); @@ -251,7 +383,7 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte continue; } // we get all the reachable scopes for this center - $reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE, $center); + $reachableScopes = $this->authorizationHelper->getReachableScopes(ActivityVoter::SEE, $center); // we get the ids for those scopes $reachablesScopesId = array_map( static fn (Scope $scope) => $scope->getId(), diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php index 8cdb83524..a046ad218 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php @@ -11,15 +11,22 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; +use Chill\ActivityBundle\Entity\Activity; +use Chill\ActivityBundle\Entity\ActivityType; +use Chill\MainBundle\Entity\UserJob; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; interface ActivityACLAwareRepositoryInterface { /** - * @return Activity[]|array + * Return all the activities associated to an accompanying period and that the user is allowed to apply the given role. + * + * + * @param array{my_activities?: bool, types?: array, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + * @return array */ - public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; + public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; /** * Return a list of activities, simplified as array (not object). @@ -31,7 +38,28 @@ interface ActivityACLAwareRepositoryInterface public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array; /** - * @return Activity[]|array + * @param array{my_activities?: bool, types?: array, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + * @return array */ - public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; + public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; + + + /** + * Return a list of the type for the activities associated to person or accompanying period + * + * @return array + */ + public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array; + + /** + * Return a list of the user job for the activities associated to person or accompanying period + * + * Associated mean the job: + * - of the creator; + * - of the user (activity.user) + * - of all the users + * + * @return array + */ + public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array; } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php index fd5d52fce..10bd1e651 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php @@ -11,9 +11,13 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; +use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityType; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\Person; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\Expr\Join; final class ActivityTypeRepository implements ActivityTypeRepositoryInterface { diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php index 2148a02f5..574faea22 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php @@ -12,12 +12,14 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; use Chill\ActivityBundle\Entity\ActivityType; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\Person; use Doctrine\Persistence\ObjectRepository; interface ActivityTypeRepositoryInterface extends ObjectRepository { /** - * @return array|ActivityType[] + * @return array */ public function findAllActive(): array; } diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index 79c946f17..17ae0598b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -86,6 +86,9 @@

{% else %} + + {{ filter|chill_render_filter_order_helper }} +
{% for activity in activities %} {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with { diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php new file mode 100644 index 000000000..e7b2117d4 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php @@ -0,0 +1,325 @@ +authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class); + $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); + $this->activityRepository = self::$container->get(ActivityRepository::class); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + $this->security = self::$container->get(Security::class); + + $this->requestStack = $requestStack = new RequestStack(); + $request = $this->prophesize(Request::class); + $request->getLocale()->willReturn('fr'); + $request->getDefaultLocale()->willReturn('fr'); + $requestStack->push($request->reveal()); + } + + /** + * @dataProvider provideDataFindByAccompanyingPeriod + */ + public function testFindByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void + { + $security = $this->prophesize(Security::class); + $security->isGranted($role, $period)->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $this->authorizationHelperForCurrentUser, + $this->centerResolverManager, + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findByAccompanyingPeriod($period, $role, $start, $limit, $orderBy, $filters); + + self::assertIsArray($actual); + } + + /** + * @dataProvider provideDataFindByAccompanyingPeriod + */ + public function testFindActivityTypeByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void + { + $security = $this->prophesize(Security::class); + $security->isGranted($role, $period)->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $this->authorizationHelperForCurrentUser, + $this->centerResolverManager, + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findActivityTypeByAssociated($period); + + self::assertIsArray($actual); + } + + /** + * @dataProvider provideDataFindByPerson + */ + public function testFindActivityTypeByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void + { + $role = ActivityVoter::SEE; + $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); + $centerResolver->resolveCenters($person)->willReturn($centers); + + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) + ->willReturn($scopes); + + $security = $this->prophesize(Security::class); + $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $authorizationHelper->reveal(), + $centerResolver->reveal(), + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); + + self::assertIsArray($actual); + } + + /** + * @dataProvider provideDataFindByPerson + */ + public function testFindByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void + { + $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); + $centerResolver->resolveCenters($person)->willReturn($centers); + + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) + ->willReturn($scopes); + + $security = $this->prophesize(Security::class); + $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $authorizationHelper->reveal(), + $centerResolver->reveal(), + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); + + self::assertIsArray($actual); + } + + public function provideDataFindByPerson(): iterable + { + $this->setUp(); + + /** @var Person $person */ + if (null === $person = $this->entityManager->createQueryBuilder() + ->select('p')->from(Person::class, 'p')->setMaxResults(1) + ->getQuery()->getSingleResult()) { + throw new \RuntimeException("person not found"); + } + + /** @var AccompanyingPeriod $period1 */ + if (null === $period1 = $this->entityManager + ->createQueryBuilder() + ->select('a') + ->from(AccompanyingPeriod::class, 'a') + ->setMaxResults(1) + ->getQuery() + ->getSingleResult()) { + throw new \RuntimeException("no period found"); + } + + /** @var AccompanyingPeriod $period2 */ + if (null === $period2 = $this->entityManager + ->createQueryBuilder() + ->select('a') + ->from(AccompanyingPeriod::class, 'a') + ->where('a.id > :pid') + ->setParameter('pid', $period1->getId()) + ->setMaxResults(1) + ->getQuery() + ->getSingleResult()) { + throw new \RuntimeException("no second period found"); + } + // add a period + $period1->addPerson($person); + $period2->addPerson($person); + $period1->getParticipationsContainsPerson($person)->first()->setEndDate( + (new \DateTime('now'))->add(new \DateInterval('P1M')) + ); + + if ([] === $types = $this->entityManager + ->createQueryBuilder() + ->select('t') + ->from(ActivityType::class, 't') + ->setMaxResults(2) + ->getQuery() + ->getResult()) { + throw new \RuntimeException("no types"); + } + + if ([] === $jobs = $this->entityManager + ->createQueryBuilder() + ->select('j') + ->from(UserJob::class, 'j') + ->setMaxResults(2) + ->getQuery() + ->getResult() + ) { + throw new \RuntimeException("no jobs found"); + } + + if (null === $user = $this->entityManager + ->createQueryBuilder() + ->select('u') + ->from(User::class, 'u') + ->setMaxResults(1) + ->getQuery() + ->getSingleResult() + ) { + throw new \RuntimeException("no user found"); + } + + if ([] === $centers = $this->entityManager->createQueryBuilder() + ->select('c')->from(Center::class, 'c')->setMaxResults(2)->getQuery() + ->getResult()) { + throw new \RuntimeException("no centers found"); + } + + if ([] === $scopes = $this->entityManager->createQueryBuilder() + ->select('s')->from(Scope::class, 's')->setMaxResults(2)->getQuery() + ->getResult()) { + throw new \RuntimeException("no scopes found"); + } + + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], []]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['my_activities' => true]]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['types' => $types]]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['jobs' => $jobs]]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; + yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; + } + + public function provideDataFindByAccompanyingPeriod(): iterable + { + $this->setUp(); + + if (null === $period = $this->entityManager + ->createQueryBuilder() + ->select('a') + ->from(AccompanyingPeriod::class, 'a') + ->setMaxResults(1) + ->getQuery() + ->getSingleResult()) { + throw new \RuntimeException("no period found"); + } + + if ([] === $types = $this->entityManager + ->createQueryBuilder() + ->select('t') + ->from(ActivityType::class, 't') + ->setMaxResults(2) + ->getQuery() + ->getResult()) { + throw new \RuntimeException("no types"); + } + + if ([] === $jobs = $this->entityManager + ->createQueryBuilder() + ->select('j') + ->from(UserJob::class, 'j') + ->setMaxResults(2) + ->getQuery() + ->getResult() + ) { + throw new \RuntimeException("no jobs found"); + } + + if (null === $user = $this->entityManager + ->createQueryBuilder() + ->select('u') + ->from(User::class, 'u') + ->setMaxResults(1) + ->getQuery() + ->getSingleResult() + ) { + throw new \RuntimeException("no user found"); + } + + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; + yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; + } +} diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 042fccc69..1229494bb 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -83,12 +83,20 @@ Third persons: Tiers non-pro. Others persons: Usagers Third parties: Tiers professionnels Users concerned: T(M)S + activity: + date: Date de l'échange Insert a document: Insérer un document Remove a document: Supprimer le document comment: Commentaire No documents: Aucun document +# activity filter in list page +activity_filter: + My activities: Mes échanges (où j'interviens) + Types: Par type d'échange + Jobs: Par métier impliqué + #timeline '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' From 0e5f1b4ab9c7d8e693332d42709841249e68a0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 23 Jun 2023 12:44:54 +0200 Subject: [PATCH 05/87] Feature: [activity list] add pagination --- .../unreleased/Feature-20230623-124438.yaml | 5 +++ .../Controller/ActivityController.php | 18 +++++--- .../Repository/ActivityACLAwareRepository.php | 43 ++++++++++++++++++- .../ActivityACLAwareRepositoryInterface.php | 10 +++++ .../Resources/views/Activity/list.html.twig | 6 ++- 5 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 .changes/unreleased/Feature-20230623-124438.yaml diff --git a/.changes/unreleased/Feature-20230623-124438.yaml b/.changes/unreleased/Feature-20230623-124438.yaml new file mode 100644 index 000000000..bc199d3bb --- /dev/null +++ b/.changes/unreleased/Feature-20230623-124438.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[activity list] add pagination to the list of activities' +time: 2023-06-23T12:44:38.879098862+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 07e4fcf62..63639c149 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -22,6 +22,7 @@ use Chill\ActivityBundle\Repository\ActivityUserJobRepository; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\LocationRepository; use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; @@ -70,6 +71,7 @@ final class ActivityController extends AbstractController private readonly TranslatorInterface $translator, private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly PaginatorFactory $paginatorFactory, ) { } @@ -251,7 +253,6 @@ final class ActivityController extends AbstractController { $view = null; $activities = []; - // TODO: add pagination [$person, $accompanyingPeriod] = $this->getEntity($request); $filter = $this->buildFilterOrder($person ?? $accompanyingPeriod); @@ -266,12 +267,14 @@ final class ActivityController extends AbstractController if ($person instanceof Person) { $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person); + $count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs); + $paginator = $this->paginatorFactory->create($count); $activities = $this->activityACLAwareRepository ->findByPerson( $person, ActivityVoter::SEE, - 0, - null, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), ['date' => 'DESC', 'id' => 'DESC'], $filterArgs ); @@ -286,17 +289,21 @@ final class ActivityController extends AbstractController } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); + $count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs); + $paginator = $this->paginatorFactory->create($count); $activities = $this->activityACLAwareRepository ->findByAccompanyingPeriod( $accompanyingPeriod, ActivityVoter::SEE, - 0, - null, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), ['date' => 'DESC', 'id' => 'DESC'], $filterArgs ); $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; + } else { + throw new \LogicException("Unsupported"); } return $this->render( @@ -306,6 +313,7 @@ final class ActivityController extends AbstractController 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, 'filter' => $filter, + 'paginator' => $paginator, ] ); } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 1fd0a57d6..1544dd764 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -27,6 +27,8 @@ use Chill\PersonBundle\Entity\Person; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; @@ -48,6 +50,33 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos ) { } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ + public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int + { + $qb = $this->buildBaseQuery($filters); + + $qb + ->select('COUNT(a)') + ->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); + + return $qb->getQuery()->getSingleScalarResult(); + } + + public function countByPerson(Person $person, string $role, array $filters = []): int + { + $qb = $this->buildBaseQuery($filters); + + $qb = $this->filterBaseQueryByPerson($qb, $person, $role); + + $qb->select('COUNT(a)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + + public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array { $qb = $this->buildBaseQuery($filters); @@ -58,7 +87,12 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos $qb->addOrderBy('a.' . $field, $order); } - $qb->setFirstResult(0)->setMaxResults(1000); + if (null !== $start) { + $qb->setFirstResult($start); + } + if (null !== $limit) { + $qb->setMaxResults($limit); + } return $qb->getQuery()->getResult(); } @@ -261,6 +295,13 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos $qb->addOrderBy('a.' . $field, $direction); } + if (null !== $start) { + $qb->setFirstResult($start); + } + if (null !== $limit) { + $qb->setMaxResults($limit); + } + return $qb->getQuery()->getResult(); } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php index a046ad218..474d8ad16 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php @@ -28,6 +28,16 @@ interface ActivityACLAwareRepositoryInterface */ public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; + /** + * @param array{my_activities?: bool, types?: array, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + */ + public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int; + + /** + * @param array{my_activities?: bool, types?: array, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + */ + public function countByPerson(Person $person, string $role, array $filters = []): int; + /** * Return a list of activities, simplified as array (not object). * diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index 17ae0598b..dd78c9396 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -80,6 +80,8 @@
+ {{ filter|chill_render_filter_order_helper }} + {% if activities|length == 0 %}

{{ "There isn't any activities."|trans }} @@ -87,8 +89,6 @@ {% else %} - {{ filter|chill_render_filter_order_helper }} -

{% for activity in activities %} {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with { @@ -99,4 +99,6 @@
{% endif %} + {{ chill_pagination(paginator) }} +
From 3e63b4abf30736c41afccb7af97fd259f3bfb6cb Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Tue, 4 Jul 2023 16:42:56 +0200 Subject: [PATCH 06/87] UX: improve FilterOrder box design --- .../translations/messages.fr.yml | 2 + .../Form/Type/Listing/FilterOrderType.php | 3 + .../Resources/public/chill/scss/forms.scss | 12 +++ .../views/FilterOrder/base.html.twig | 81 ++++++++----------- 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index c53a04f31..d37b3488f 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -96,6 +96,8 @@ activity_filter: My activities: Mes échanges (où j'interviens) Types: Par type d'échange Jobs: Par métier impliqué + By: Filtrer par + Search: Chercher dans la liste #timeline '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index 16038515d..1f373400c 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -38,6 +38,9 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $builder->add('q', SearchType::class, [ 'label' => false, 'required' => false, + 'attr' => [ + 'placeholder' => 'activity_filter.Search', + ] ]); } diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss index 0ae568244..cd81f36dc 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss @@ -42,3 +42,15 @@ form { font-weight: 700; margin-bottom: .375em; } + +.chill_filter_order { + background: $gray-100; /* + border: 3px dashed $white; + background: repeating-linear-gradient( + -45deg, + $gray-100, + $gray-100 2px, + $white 2px, + $white 6px + ); */ +} \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index d4a6bbdd4..8faef5d14 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -1,102 +1,87 @@ {{ form_start(form) }} -
-
+ {% set btnSubmit = 0 %} +
+
{% if form.vars.has_search_box %} -
-
- {{ form_widget(form.q)}} - +
+
+ {{ form_widget(form.q) }} +
{% endif %}
{% if form.dateRanges is defined %} + {% set btnSubmit = 1 %} {% if form.dateRanges|length > 0 %} {% for dateRangeName, _o in form.dateRanges %} -
+
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} -
{{ form_label(form.dateRanges[dateRangeName])}} -
{% endif %} -
-
+
+
{{ 'chill_calendar.From'|trans }} {{ form_widget(form.dateRanges[dateRangeName]['from']) }} {{ 'chill_calendar.To'|trans }} {{ form_widget(form.dateRanges[dateRangeName]['to']) }}
-
- -
{% endfor %} {% endif %} {% endif %} {% if form.checkboxes is defined %} + {% set btnSubmit = 1 %} {% if form.checkboxes|length > 0 %} {% for checkbox_name, options in form.checkboxes %} -
-
+
+
{{ 'activity_filter.By'|trans }}
+
{% for c in form['checkboxes'][checkbox_name].children %} -
- {{ form_widget(c) }} - {{ form_label(c) }} -
+ {{ form_widget(c) }} + {{ form_label(c) }} {% endfor %}
- {% if loop.last %} -
-
-
    -
  • - -
  • -
-
-
- {% endif %} {% endfor %} {% endif %} {% endif %} {% if form.entity_choices is defined %} + {% set btnSubmit = 1 %} {% if form.entity_choices |length > 0 %} {% for checkbox_name, options in form.entity_choices %} -
+
{% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} -
- {{ form_label(form.entity_choices[checkbox_name])}} -
+ {{ form_label(form.entity_choices[checkbox_name])}} {% endif %} -
+
{% for c in form['entity_choices'][checkbox_name].children %} -
- {{ form_widget(c) }} - {{ form_label(c) }} -
+ {{ form_widget(c) }} + {{ form_label(c) }} {% endfor %}
-
- -
{% endfor %} {% endif %} {% endif %} {% if form.single_checkboxes is defined %} + {% set btnSubmit = 1 %} {% for name, _o in form.single_checkboxes %} -
-
+
+
{{ 'activity_filter.By'|trans }}
+
{{ form_widget(form.single_checkboxes[name]) }}
-
- -
{% endfor %} {% endif %} + + {% if btnSubmit == 1 %} +
+ +
+ {% endif %}
{% for k,v in otherParameters %} From 7f9738975cd6690b20ea7c886562342994c9c5f3 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Tue, 4 Jul 2023 17:53:08 +0200 Subject: [PATCH 07/87] UX: improve FilterOrder box design --- src/Bundle/ChillActivityBundle/translations/messages.fr.yml | 1 + .../Resources/views/FilterOrder/base.html.twig | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index d37b3488f..3099e99b0 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -98,6 +98,7 @@ activity_filter: Jobs: Par métier impliqué By: Filtrer par Search: Chercher dans la liste + By date: Filtrer par date #timeline '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 8faef5d14..b2673b60c 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -18,8 +18,10 @@
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} {{ form_label(form.dateRanges[dateRangeName])}} + {% else %} +
{{ 'activity_filter.By date'|trans }}
{% endif %} -
+
{{ 'chill_calendar.From'|trans }} {{ form_widget(form.dateRanges[dateRangeName]['from']) }} From 145c1df313e0328830fe406c12b1afcf03b070dd Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 5 Jul 2023 09:43:13 +0200 Subject: [PATCH 08/87] cleaning --- .../Resources/public/chill/scss/forms.scss | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss index cd81f36dc..a517a5516 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss @@ -44,13 +44,5 @@ form { } .chill_filter_order { - background: $gray-100; /* - border: 3px dashed $white; - background: repeating-linear-gradient( - -45deg, - $gray-100, - $gray-100 2px, - $white 2px, - $white 6px - ); */ + background: $gray-100; } \ No newline at end of file From 25d4b6acbb2b5d547abb38072f69da7f764e4141 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 10:54:37 +0200 Subject: [PATCH 09/87] [FEATURE] allow adding of user filters in filter order - template still to be done --- .../Form/Type/Listing/FilterOrderType.php | 30 ++++++++++++++++++ .../Templating/Listing/FilterOrderHelper.php | 31 ++++++++++++++++++- .../Listing/FilterOrderHelperBuilder.php | 23 ++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index 16038515d..9aebab144 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type\Listing; use Chill\MainBundle\Form\Type\ChillDateType; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -120,6 +121,35 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $builder->add($singleCheckBoxBuilder); } + + if ([] !== $helper->getUserPickers()) { + $userPickersBuilder = $builder->create('userPicker', null, ['compound' => true]); + + foreach ($helper->getUserPickers() as $name => [ + 'label' => $label, 'options' => $options + ]) { + $userPicker = $userPickersBuilder->create($name, null, [ + 'compound' => true, + 'label' => $label, + ]); + + $userPicker->add( + $name, + PickUserDynamicType::class, + [ + 'multiple' => true, + 'label' => $label, + ...$options, + ] + ); + + + $userPickersBuilder->add($userPicker); + } + + $builder->add($userPickersBuilder); + } + } public function buildView(FormView $view, FormInterface $form, array $options) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 28cc8e331..f12c23299 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -52,6 +52,11 @@ class FilterOrderHelper */ private array $entityChoices = []; + /** + * @var array + */ + private array $userPickers = []; + public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack @@ -82,6 +87,14 @@ class FilterOrderHelper return $this->entityChoices; } + public function addUserPickers(string $name, ?string $label = null, array $options = []): self + { + $this->userPickers[$name] = ['label' => $label, 'options' => $options]; + + return $this; + } + + public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { $missing = count($choices) - count($trans) - 1; @@ -116,6 +129,16 @@ class FilterOrderHelper ->handleRequest($this->requestStack->getCurrentRequest()); } + public function getUserPickers(): array + { + return $this->userPickers; + } + + public function getUserPickerData(string $name): array + { + return $this->getFormData()['userPickers'][$name]; + } + public function getCheckboxData(string $name): array { return $this->getFormData()['checkboxes'][$name]; @@ -180,7 +203,8 @@ class FilterOrderHelper 'checkboxes' => [], 'dateRanges' => [], 'single_checkboxes' => [], - 'entity_choices' => [] + 'entity_choices' => [], + 'user_pickers' => [] ]; if ($this->hasSearchBox()) { @@ -204,7 +228,12 @@ class FilterOrderHelper $r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null; } + foreach ($this->userPickers as $name => $u) { + $r['user_pickers'][$name] = ($u['options']['multiple'] ?? true) ? [] : null; + } + return $r; + } private function getFormData(): array diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index e176e27c6..6fd0eeb42 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -37,6 +37,11 @@ class FilterOrderHelperBuilder */ private array $entityChoices = []; + /** + * @var array + */ + private array $userPickers = []; + public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack @@ -83,6 +88,13 @@ class FilterOrderHelperBuilder return $this; } + public function addUserPickers(string $name, ?string $label = null, ?array $options = []): self + { + $this->userPickers[$name] = ['label' => $label, 'options' => $options]; + + return $this; + } + public function build(): FilterOrderHelper { $helper = new FilterOrderHelper( @@ -124,6 +136,17 @@ class FilterOrderHelperBuilder $helper->addDateRange($name, $label, $from, $to); } + + foreach ( + $this->userPickers as $name => [ + 'label' => $label, + 'options' => $options + ] + ) { + $helper->addUserPickers($name, $label, $options); + } + + return $helper; } } From 0d626fb3459a0b30e847b315618851e203db68ef Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 10:55:30 +0200 Subject: [PATCH 10/87] [FEATURE] implement user filter in orderFilterHelper for tasks --- src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php | 2 ++ .../Resources/views/SingleTask/List/index.html.twig | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index 2ba9488b6..58d2a406e 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -299,6 +299,7 @@ final class SingleTaskController extends AbstractController $this->denyAccessUnlessGranted(TaskVoter::SHOW, null); $filterOrder = $this->buildFilterOrder(); + $flags = array_merge( $filterOrder->getCheckboxData('status'), array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states')) @@ -680,6 +681,7 @@ final class SingleTaskController extends AbstractController ->addSearchBox() ->addCheckbox('status', $statuses, $statuses, $statusTrans) ->addCheckbox('states', $states, ['new', 'in_progress']) + ->addUserPickers('userPicker', 'userPicker', ['multiple' => True]) ->build(); } diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig index 8fe45959e..b479eb948 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig @@ -27,8 +27,10 @@ {% block css %} {{ parent() }} {{ encore_entry_link_tags('page_task_list') }} + {{ encore_entry_link_tags('mod_pickentity_type') }} {% endblock %} {% block js %} {{ parent() }} {{ encore_entry_script_tags('page_task_list') }} + {{ encore_entry_script_tags('mod_pickentity_type') }} {% endblock %} From 4da7040a49b4a05af0e37434052cc1d457806629 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 12:38:42 +0200 Subject: [PATCH 11/87] FEATURE [user filter] implement query. Selecting multiple users doesn't work --- .../Form/Type/Listing/FilterOrderType.php | 2 +- .../views/FilterOrder/base.html.twig | 18 +++++++++++- .../Templating/Listing/FilterOrderHelper.php | 5 ++-- .../Listing/FilterOrderHelperBuilder.php | 4 +-- .../Controller/SingleTaskController.php | 8 +++-- .../SingleTaskAclAwareRepository.php | 29 ++++++++++++++++--- .../SingleTaskAclAwareRepositoryInterface.php | 4 ++- .../translations/messages.fr.yml | 1 + 8 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index a5166e322..c6744f852 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -126,7 +126,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType } if ([] !== $helper->getUserPickers()) { - $userPickersBuilder = $builder->create('userPicker', null, ['compound' => true]); + $userPickersBuilder = $builder->create('user_pickers', null, ['compound' => true]); foreach ($helper->getUserPickers() as $name => [ 'label' => $label, 'options' => $options diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index b2673b60c..fa35cdbf3 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -67,6 +67,22 @@ {% endfor %} {% endif %} {% endif %} + {% if form.user_pickers is defined %} + {% set btnSubmit = 1 %} + {% if form.user_pickers|length > 0 %} + {% for name, options in form.user_pickers %} +
+
+ {% for p in form['user_pickers'][name].children %} + {{ form_widget(p) }} + {{ form_label(p) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.single_checkboxes is defined %} {% set btnSubmit = 1 %} {% for name, _o in form.single_checkboxes %} @@ -78,7 +94,7 @@
{% endfor %} {% endif %} - + {% if btnSubmit == 1 %}
diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index bdec463bd..026c55ef6 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -87,7 +87,7 @@ class FilterOrderHelper return $this->entityChoices; } - public function addUserPickers(string $name, ?string $label = null, array $options = []): self + public function addUserPicker(string $name, ?string $label = null, array $options = []): self { $this->userPickers[$name] = ['label' => $label, 'options' => $options]; @@ -136,7 +136,8 @@ class FilterOrderHelper public function getUserPickerData(string $name): array { - return $this->getFormData()['userPickers'][$name]; + dump($this->getFormData()['user_pickers']); + return $this->getFormData()['user_pickers'][$name]; } public function getCheckboxData(string $name): array diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index 6fd0eeb42..86180d06e 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -88,7 +88,7 @@ class FilterOrderHelperBuilder return $this; } - public function addUserPickers(string $name, ?string $label = null, ?array $options = []): self + public function addUserPicker(string $name, ?string $label = null, ?array $options = []): self { $this->userPickers[$name] = ['label' => $label, 'options' => $options]; @@ -143,7 +143,7 @@ class FilterOrderHelperBuilder 'options' => $options ] ) { - $helper->addUserPickers($name, $label, $options); + $helper->addUserPicker($name, $label, $options); } diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index 58d2a406e..e60a58145 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -300,13 +300,16 @@ final class SingleTaskController extends AbstractController $filterOrder = $this->buildFilterOrder(); + $filteredUsers = $filterOrder->getUserPickerData('userPicker'); + $flags = array_merge( $filterOrder->getCheckboxData('status'), array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states')) ); $nb = $this->singleTaskAclAwareRepository->countByAllViewable( $filterOrder->getQueryString(), - $flags + $flags, + $filteredUsers ); $paginator = $this->paginatorFactory->create($nb); @@ -314,6 +317,7 @@ final class SingleTaskController extends AbstractController $tasks = $this->singleTaskAclAwareRepository->findByAllViewable( $filterOrder->getQueryString(), $flags, + $filteredUsers, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage(), [ @@ -681,7 +685,7 @@ final class SingleTaskController extends AbstractController ->addSearchBox() ->addCheckbox('status', $statuses, $statuses, $statusTrans) ->addCheckbox('states', $states, ['new', 'in_progress']) - ->addUserPickers('userPicker', 'userPicker', ['multiple' => True]) + ->addUserPicker('userPicker', 'Filter by user', ['multiple' => True, 'required' => False]) ->build(); } diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php index 0efc16085..ef30a7783 100644 --- a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php @@ -51,7 +51,8 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function buildBaseQuery( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): QueryBuilder { $qb = $this->em->createQueryBuilder(); $qb @@ -62,6 +63,24 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository ->setParameter('pattern', '%' . $pattern . '%'); } + if (count($users) > 0) { + $orXUser = $qb->expr()->orX(); + + foreach ($users as $key => $user) { + $orXUser->add( + $qb->expr()->eq('t.assignee', ':user') + ); + + $qb->setParameter('user', $user); + } + + if ($orXUser->count() > 0) { + $qb->andWhere($orXUser); + } + + return $qb; + } + if (count($flags) > 0) { $orXDate = $qb->expr()->orX(); $orXState = $qb->expr()->orX(); @@ -183,9 +202,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function countByAllViewable( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): int { - $qb = $this->buildBaseQuery($pattern, $flags); + $qb = $this->buildBaseQuery($pattern, $flags, $users); return $this ->addACLGlobal($qb) @@ -231,11 +251,12 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function findByAllViewable( ?string $pattern = null, ?array $flags = [], + ?array $users = [], ?int $start = 0, ?int $limit = 50, ?array $orderBy = [] ): array { - $qb = $this->buildBaseQuery($pattern, $flags); + $qb = $this->buildBaseQuery($pattern, $flags, $users); $qb = $this->addACLGlobal($qb); return $this->getResult($qb, $start, $limit, $orderBy); diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php index 57c57e592..7d2870c67 100644 --- a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php @@ -18,7 +18,8 @@ interface SingleTaskAclAwareRepositoryInterface { public function countByAllViewable( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): int; public function countByCourse( @@ -38,6 +39,7 @@ interface SingleTaskAclAwareRepositoryInterface public function findByAllViewable( ?string $pattern = null, ?array $flags = [], + ?array $users = [], ?int $start = 0, ?int $limit = 50, ?array $orderBy = [] diff --git a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml index d437baac2..599ca8640 100644 --- a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml @@ -65,6 +65,7 @@ Not assigned: Aucun utilisateur assigné For person: Pour By: Par Any tasks: Aucune tâche +Filter by user: Filtrer par utilisateur(s) # transitions - default task definition "new": "nouvelle" From 1ee0e8e350381e039f04171019f2a9584394412e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 13:35:06 +0200 Subject: [PATCH 12/87] DX phpstan and csfixer --- .../Form/Type/Listing/FilterOrderType.php | 13 +++---------- .../Resources/views/FilterOrder/base.html.twig | 12 +++++++----- .../Templating/Listing/FilterOrderHelper.php | 1 - .../Templating/Listing/FilterOrderHelperBuilder.php | 6 +++--- .../Controller/SingleTaskController.php | 2 +- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index c6744f852..4e41a1740 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -129,25 +129,18 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $userPickersBuilder = $builder->create('user_pickers', null, ['compound' => true]); foreach ($helper->getUserPickers() as $name => [ - 'label' => $label, 'options' => $options + 'label' => $label, 'options' => $opts ]) { - $userPicker = $userPickersBuilder->create($name, null, [ - 'compound' => true, - 'label' => $label, - ]); - $userPicker->add( + $userPickersBuilder->add( $name, PickUserDynamicType::class, [ 'multiple' => true, 'label' => $label, - ...$options, + ...$opts, ] ); - - - $userPickersBuilder->add($userPicker); } $builder->add($userPickersBuilder); diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index fa35cdbf3..9a9a11fbd 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -69,14 +69,16 @@ {% endif %} {% if form.user_pickers is defined %} {% set btnSubmit = 1 %} - {% if form.user_pickers|length > 0 %} + {% if form.user_pickers.children|length > 0 %} {% for name, options in form.user_pickers %}
+ {% if form.user_pickers[name].vars.label is not same as(false) %} + {{ form_label(form.user_pickers[name]) }} + {% else %} + {{ form_label(form.user_pickers[name].vars.label) }} + {% endif %}
- {% for p in form['user_pickers'][name].children %} - {{ form_widget(p) }} - {{ form_label(p) }} - {% endfor %} + {{ form_widget(form.user_pickers[name]) }}
{% endfor %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 026c55ef6..8554b4431 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -136,7 +136,6 @@ class FilterOrderHelper public function getUserPickerData(string $name): array { - dump($this->getFormData()['user_pickers']); return $this->getFormData()['user_pickers'][$name]; } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index 86180d06e..d9a505dee 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -139,9 +139,9 @@ class FilterOrderHelperBuilder foreach ( $this->userPickers as $name => [ - 'label' => $label, - 'options' => $options - ] + 'label' => $label, + 'options' => $options + ] ) { $helper->addUserPicker($name, $label, $options); } diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index e60a58145..22e49704f 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -685,7 +685,7 @@ final class SingleTaskController extends AbstractController ->addSearchBox() ->addCheckbox('status', $statuses, $statuses, $statusTrans) ->addCheckbox('states', $states, ['new', 'in_progress']) - ->addUserPicker('userPicker', 'Filter by user', ['multiple' => True, 'required' => False]) + ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]) ->build(); } From 4e934653be7641605ac52070e3529663c78e00f7 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 14:12:43 +0200 Subject: [PATCH 13/87] DX changie added --- .changes/unreleased/Feature-20230705-140336.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Feature-20230705-140336.yaml diff --git a/.changes/unreleased/Feature-20230705-140336.yaml b/.changes/unreleased/Feature-20230705-140336.yaml new file mode 100644 index 000000000..3ce7f3c0f --- /dev/null +++ b/.changes/unreleased/Feature-20230705-140336.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Allow filtering on the basis of a user within general tasks list +time: 2023-07-05T14:03:36.664880092+02:00 +custom: + Issue: "" From 52d51264bab3a096be592a8df31f83e3f0b4dca4 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 14:57:28 +0200 Subject: [PATCH 14/87] FIX [query][user filter] avoid replacement of user parameter in query --- .../Repository/SingleTaskAclAwareRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php index ef30a7783..d1652cc89 100644 --- a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php @@ -68,10 +68,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository foreach ($users as $key => $user) { $orXUser->add( - $qb->expr()->eq('t.assignee', ':user') + $qb->expr()->eq('t.assignee', ':user_' . $key) ); - $qb->setParameter('user', $user); + $qb->setParameter('user_' . $key, $user); } if ($orXUser->count() > 0) { From 4b25970ce0a43e82531280940c6507da8f071b41 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 15:35:13 +0200 Subject: [PATCH 15/87] FEATURE [filter] start implementation of social action filter --- .../AccompanyingCourseWorkController.php | 38 ++++++++++++++++++- .../AccompanyingPeriodWorkRepository.php | 2 +- .../translations/messages.fr.yml | 3 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 8d15ca30f..75d0bae7c 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -11,9 +11,14 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; +use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Psr\Log\LoggerInterface; @@ -38,18 +43,22 @@ class AccompanyingCourseWorkController extends AbstractController private AccompanyingPeriodWorkRepository $workRepository; + private TranslatableStringHelperInterface $translatableStringHelper; + public function __construct( TranslatorInterface $trans, SerializerInterface $serializer, AccompanyingPeriodWorkRepository $workRepository, PaginatorFactory $paginator, - LoggerInterface $chillLogger + LoggerInterface $chillLogger, + TranslatableStringHelperInterface $translatableStringHelper ) { $this->trans = $trans; $this->serializer = $serializer; $this->workRepository = $workRepository; $this->paginator = $paginator; $this->chillLogger = $chillLogger; + $this->translatableStringHelper = $translatableStringHelper; } /** @@ -162,11 +171,21 @@ class AccompanyingCourseWorkController extends AbstractController { $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period); + $filter = $this->buildFilterOrder($period); + + $filterData = [ + 'types' => $filter->getEntityChoiceData('typesFilter'), + 'before' => $filter->getDateRangeData('dateFilter')['to'], + 'after' => $filter->getDateRangeData('dateFilter')['from'], + 'user' => $filter->getUserPickerData('userFilter') + ]; + $totalItems = $this->workRepository->countByAccompanyingPeriod($period); $paginator = $this->paginator->create($totalItems); $works = $this->workRepository->findByAccompanyingPeriodOpenFirst( $period, + $filterData, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() ); @@ -210,4 +229,21 @@ class AccompanyingCourseWorkController extends AbstractController ->add('submit', SubmitType::class, ['label' => 'Delete']) ->getForm(); } + + private function buildFilterOrder($associatedPeriod): FilterOrderHelper + { + + $filterBuilder = $this->filterOrderHelperFactory->create(self::class); + $types = $this->workRepository->findByAccompanyingPeriod($associatedPeriod); + + $filterBuilder + ->addDateRange('dateFilter', 'accompanying_course_work.date_filter') + ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialAction::class, $types, [ + 'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle()) + ]) + ->addUserPicker('userFilter', 'accompanying_course_work.user_filter') + ; + + return $filterBuilder->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index bf2d34aae..1bd9b0f4e 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -96,7 +96,7 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository * * @return AccompanyingPeriodWork[] */ - public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, int $limit = 10, int $offset = 0): array + public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, $filters, int $limit = 10, int $offset = 0): array { $rsm = new ResultSetMappingBuilder($this->em); $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 01383c050..f783c403e 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -912,6 +912,9 @@ accompanying_course_work: social_evaluation: Évaluation private_comment: Commentaire privé timeSpent: Temps de rédaction + date_filter: Filtrer par date + types_filter: Filtrer par type d'action + user_filter: Filtrer par intervenant # From 6c58e7eb3e13a3421250263f69043740787e0c8d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 15:49:50 +0200 Subject: [PATCH 16/87] DX phpstan and cs-fixer --- .../AccompanyingCourseWorkController.php | 34 +++++-------------- .../AccompanyingPeriodWorkRepository.php | 1 + 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 75d0bae7c..9abf1677d 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Controller; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -33,32 +34,15 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingCourseWorkController extends AbstractController { - private LoggerInterface $chillLogger; - - private PaginatorFactory $paginator; - - private SerializerInterface $serializer; - - private TranslatorInterface $trans; - - private AccompanyingPeriodWorkRepository $workRepository; - - private TranslatableStringHelperInterface $translatableStringHelper; - public function __construct( - TranslatorInterface $trans, - SerializerInterface $serializer, - AccompanyingPeriodWorkRepository $workRepository, - PaginatorFactory $paginator, - LoggerInterface $chillLogger, - TranslatableStringHelperInterface $translatableStringHelper + private readonly TranslatorInterface $trans, + private readonly SerializerInterface $serializer, + private readonly AccompanyingPeriodWorkRepository $workRepository, + private readonly PaginatorFactory $paginator, + private readonly LoggerInterface $chillLogger, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory ) { - $this->trans = $trans; - $this->serializer = $serializer; - $this->workRepository = $workRepository; - $this->paginator = $paginator; - $this->chillLogger = $chillLogger; - $this->translatableStringHelper = $translatableStringHelper; } /** @@ -238,7 +222,7 @@ class AccompanyingCourseWorkController extends AbstractController $filterBuilder ->addDateRange('dateFilter', 'accompanying_course_work.date_filter') - ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialAction::class, $types, [ + ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialWork\SocialAction::class, $types, [ 'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle()) ]) ->addUserPicker('userFilter', 'accompanying_course_work.user_filter') diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 1bd9b0f4e..60a00e12a 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -95,6 +95,7 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository * * then, closed works * * @return AccompanyingPeriodWork[] + * @param mixed $filters */ public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, $filters, int $limit = 10, int $offset = 0): array { From 61982634a6203875f6bcaf0fd30fd454db204729 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Jul 2023 16:05:51 +0200 Subject: [PATCH 17/87] FEATURE add filter to the template --- .../Controller/AccompanyingCourseWorkController.php | 3 ++- .../Resources/views/AccompanyingCourseWork/index.html.twig | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 9abf1677d..1f2680f9e 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -178,6 +178,7 @@ class AccompanyingCourseWorkController extends AbstractController 'accompanyingCourse' => $period, 'works' => $works, 'paginator' => $paginator, + 'filter' => $filter ]); } @@ -225,7 +226,7 @@ class AccompanyingCourseWorkController extends AbstractController ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialWork\SocialAction::class, $types, [ 'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle()) ]) - ->addUserPicker('userFilter', 'accompanying_course_work.user_filter') + ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false]) ; return $filterBuilder->build(); diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig index ab1989f63..1cedfa694 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig @@ -5,18 +5,23 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_pickentity_type') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_pickentity_type') }} {% endblock %} + {% block content %}

{{ block('title') }}

+ {{ filter|chill_render_filter_order_helper }} + {% if works|length == 0 %}

{{ 'accompanying_course_work.Any work'|trans }}

{% else %} From c04fd66163f2014ac54362f56bafebca9f38e1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 22:20:27 +0200 Subject: [PATCH 18/87] do not show filter on job or activity type if less than 2 possibilities --- .../Controller/ActivityController.php | 39 +++++++++++-------- .../Templating/Listing/FilterOrderHelper.php | 20 ++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 63639c149..9b6e69bf0 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -259,8 +259,8 @@ final class ActivityController extends AbstractController $filterArgs = [ 'my_activities' => $filter->getSingleCheckboxData('my_activities'), - 'types' => $filter->getEntityChoiceData('activity_types'), - 'jobs' => $filter->getEntityChoiceData('jobs'), + 'types' => $filter->hasEntityChoice('activity_type') ? $filter->getEntityChoiceData('activity_types') : [], + 'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [], 'before' => $filter->getDateRangeData('activity_date')['to'], 'after' => $filter->getDateRangeData('activity_date')['from'], ]; @@ -327,21 +327,28 @@ final class ActivityController extends AbstractController $filterBuilder ->addDateRange('activity_date', 'activity.date') - ->addSingleCheckbox('my_activities', 'activity_filter.My activities') - ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ - 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { - $text = match ($activityType->hasCategory()) { - true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', - false => '', - }; + ->addSingleCheckbox('my_activities', 'activity_filter.My activities'); - return $text . $this->translatableStringHelper->localize($activityType->getName()); - } - ]) - ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ - 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) - ]) - ; + if (1 < count($types)) { + $filterBuilder + ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ + 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { + $text = match ($activityType->hasCategory()) { + true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', + false => '', + }; + + return $text . $this->translatableStringHelper->localize($activityType->getName()); + } + ]); + } + + if (1 < count($jobs)) { + $filterBuilder + ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ + 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) + ]); + } return $filterBuilder->build(); } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 28cc8e331..5c9971890 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -116,16 +116,31 @@ class FilterOrderHelper ->handleRequest($this->requestStack->getCurrentRequest()); } + public function hasCheckboxData(string $name): bool + { + return array_key_exists($name, $this->checkboxes); + } + public function getCheckboxData(string $name): array { return $this->getFormData()['checkboxes'][$name]; } + public function hasSingleCheckboxData(string $name): bool + { + return array_key_exists($name, $this->singleCheckbox); + } + public function getSingleCheckboxData(string $name): ?bool { return $this->getFormData()['single_checkboxes'][$name]; } + public function hasEntityChoice(string $name): bool + { + return array_key_exists($name, $this->entityChoices); + } + public function getEntityChoiceData($name): mixed { return $this->getFormData()['entity_choices'][$name]; @@ -144,6 +159,11 @@ class FilterOrderHelper return $this->singleCheckbox; } + public function hasDateRangeData(string $name): bool + { + return array_key_exists($name, $this->dateRanges); + } + /** * @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable} */ From 20d5fabc1876e65ca07ffb18b1cfa477ae1fc2c4 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 6 Jul 2023 13:39:08 +0200 Subject: [PATCH 19/87] [repository][action filter] integrating filters in repository --- .../AccompanyingCourseWorkController.php | 4 +- .../AccompanyingPeriodWorkRepository.php | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 1f2680f9e..c56489afd 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -161,7 +161,7 @@ class AccompanyingCourseWorkController extends AbstractController 'types' => $filter->getEntityChoiceData('typesFilter'), 'before' => $filter->getDateRangeData('dateFilter')['to'], 'after' => $filter->getDateRangeData('dateFilter')['from'], - 'user' => $filter->getUserPickerData('userFilter') + 'users' => $filter->getUserPickerData('userFilter') ]; $totalItems = $this->workRepository->countByAccompanyingPeriod($period); @@ -219,7 +219,7 @@ class AccompanyingCourseWorkController extends AbstractController { $filterBuilder = $this->filterOrderHelperFactory->create(self::class); - $types = $this->workRepository->findByAccompanyingPeriod($associatedPeriod); + $types = $this->workRepository->findActionTypeByPeriod($associatedPeriod); $filterBuilder ->addDateRange('dateFilter', 'accompanying_course_work.date_filter') diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 60a00e12a..a5e27afd5 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -108,8 +108,32 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, startdate DESC, enddate DESC, - id DESC - LIMIT :limit OFFSET :offset"; + id DESC"; + + // implement filters + + if([] !== ($filters['types'] ?? [])) + { + $sql .= "AND WHERE w.socialAction IN (:types)"; + } + + if([] !== ($filters['users'] ?? [])) + { + $sql .= "AND WHERE w.createdBy IN (:users)"; + + foreach ($filters['users'] as $key => $user) { + $sql .= "OR :user_" . $key . " IN w.referrers)"; + + $nq = $this->em->createNativeQuery($sql, $rsm) + ->setParameter(':user_' . $key); + } + + // ... to be continued + } + + // set limit and offset + + $sql .= " LIMIT :limit OFFSET :offset"; $nq = $this->em->createNativeQuery($sql, $rsm) ->setParameter('periodId', $period->getId(), Types::INTEGER) @@ -119,6 +143,36 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository return $nq->getResult(); } + /** + * Return a list of types of social actions associated to the accompanying period + * + * @return array + */ + public function findActionTypeByPeriod(AccompanyingPeriod $period): array + { + $in = $this->em->createQueryBuilder(); + $in + ->select('1') + ->from(AccompanyingPeriodWork::class, 'apw'); + + + $in->andWhere('apw.accompanyingPeriod = :period')->setParameter('period', $period); + + + // join between the embedded exist query and the main query + $in->andWhere('apw.socialAction = sa'); + + $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); + $qb + ->select('sa') + ->from(SocialAction::class, 'sa') + ->where( + $qb->expr()->exists($in->getDQL()) + ); + + return $qb->getQuery()->getResult(); + } + public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array { return $this->buildQueryNearEndDateByUser($user, $since, $until) From cc97199c5deceb7c98bca0af26c659a9b5ea149d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 6 Jul 2023 13:40:25 +0200 Subject: [PATCH 20/87] DX added changie --- .changes/unreleased/Feature-20230706-134010.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Feature-20230706-134010.yaml diff --git a/.changes/unreleased/Feature-20230706-134010.yaml b/.changes/unreleased/Feature-20230706-134010.yaml new file mode 100644 index 000000000..73a0727fc --- /dev/null +++ b/.changes/unreleased/Feature-20230706-134010.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Adding OrderFilter to the list of social actions. +time: 2023-07-06T13:40:10.339001208+02:00 +custom: + Issue: "120" From 7ccff61c254cea360b68bf33d74ea0988ee1a5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:17:36 +0200 Subject: [PATCH 21/87] Refactor ListAccompanyingPeriod to use a helper for most of the work --- .../Export/Export/ListAccompanyingPeriod.php | 313 +----------------- .../Helper/ListAccompanyingPeriodHelper.php | 306 +++++++++++++++++ .../translations/messages.fr.yml | 2 +- 3 files changed, 315 insertions(+), 306 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php index af66ab312..3f7821bbc 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person\PersonCenterHistory; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Export\Declarations; +use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; use Chill\PersonBundle\Security\Authorization\PersonVoter; @@ -45,95 +46,13 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function strlen; -class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface +final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface { - private const FIELDS = [ - 'id', - 'step', - 'stepSince', - 'openingDate', - 'closingDate', - 'referrer', - 'referrerSince', - 'administrativeLocation', - 'locationIsPerson', - 'locationIsTemp', - 'locationPersonName', - 'locationPersonId', - 'origin', - 'closingMotive', - 'confidential', - 'emergency', - 'intensity', - 'job', - 'isRequestorPerson', - 'isRequestorThirdParty', - 'requestorPerson', - 'requestorPersonId', - 'requestorThirdParty', - 'requestorThirdPartyId', - 'scopes', - 'socialIssues', - 'createdAt', - 'createdBy', - 'updatedAt', - 'updatedBy', - ]; - - private ExportAddressHelper $addressHelper; - - private DateTimeHelper $dateTimeHelper; - - private EntityManagerInterface $entityManager; - - private PersonRenderInterface $personRender; - - private PersonRepository $personRepository; - - private RollingDateConverterInterface $rollingDateConverter; - - private SocialIssueRender $socialIssueRender; - - private SocialIssueRepository $socialIssueRepository; - - private ThirdPartyRender $thirdPartyRender; - - private ThirdPartyRepository $thirdPartyRepository; - - private TranslatableStringHelperInterface $translatableStringHelper; - - private TranslatorInterface $translator; - - private UserHelper $userHelper; - public function __construct( - ExportAddressHelper $addressHelper, - DateTimeHelper $dateTimeHelper, - EntityManagerInterface $entityManager, - PersonRenderInterface $personRender, - PersonRepository $personRepository, - ThirdPartyRepository $thirdPartyRepository, - ThirdPartyRender $thirdPartyRender, - SocialIssueRepository $socialIssueRepository, - SocialIssueRender $socialIssueRender, - TranslatableStringHelperInterface $translatableStringHelper, - TranslatorInterface $translator, - RollingDateConverterInterface $rollingDateConverter, - UserHelper $userHelper + private EntityManagerInterface $entityManager, + private RollingDateConverterInterface $rollingDateConverter, + private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper, ) { - $this->addressHelper = $addressHelper; - $this->dateTimeHelper = $dateTimeHelper; - $this->entityManager = $entityManager; - $this->personRender = $personRender; - $this->personRepository = $personRepository; - $this->socialIssueRender = $socialIssueRender; - $this->socialIssueRepository = $socialIssueRepository; - $this->thirdPartyRender = $thirdPartyRender; - $this->thirdPartyRepository = $thirdPartyRepository; - $this->translatableStringHelper = $translatableStringHelper; - $this->translator = $translator; - $this->rollingDateConverter = $rollingDateConverter; - $this->userHelper = $userHelper; } public function buildForm(FormBuilderInterface $builder) @@ -169,141 +88,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface public function getLabels($key, array $values, $data) { - if (substr($key, 0, strlen('address_fields')) === 'address_fields') { - return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); - } - - switch ($key) { - case 'stepSince': - case 'openingDate': - case 'closingDate': - case 'referrerSince': - case 'createdAt': - case 'updatedAt': - return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); - - case 'origin': - case 'closingMotive': - case 'job': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); - }; - - case 'locationPersonName': - case 'requestorPerson': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value || null === $person = $this->personRepository->find($value)) { - return ''; - } - - return $this->personRender->renderString($person, []); - }; - - case 'requestorThirdParty': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { - return ''; - } - - return $this->thirdPartyRender->renderString($thirdparty, []); - }; - - case 'scopes': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return implode( - '|', - array_map( - fn ($s) => $this->translatableStringHelper->localize($s), - json_decode($value, true, 512, JSON_THROW_ON_ERROR) - ) - ); - }; - - case 'socialIssues': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return implode( - '|', - array_map( - fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), - json_decode($value, true, 512, JSON_THROW_ON_ERROR) - ) - ); - }; - - case 'step': - return fn ($value) => match ($value) { - '_header' => 'export.list.acp.step', - null => '', - AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), - AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), - AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), - AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), - AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), - default => $value, - }; - - case 'intensity': - return fn ($value) => match ($value) { - '_header' => 'export.list.acp.intensity', - null => '', - AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), - AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), - default => $value, - }; - - default: - return static function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return $value; - }; - } + return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); } public function getQueryKeys($data) { - return array_merge( - self::FIELDS, - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') - ); + return $this->listAccompanyingPeriodHelper->getQueryKeys($data); } public function getResult($query, $data) @@ -341,7 +131,7 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface ->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT) ->setParameter('authorized_centers', $centers); - $this->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); + $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); return $qb; } @@ -357,91 +147,4 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface Declarations::ACP_TYPE, ]; } - - private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void - { - // add the regular fields - foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { - $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); - } - - // add the field which are simple association - foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { - $qb - ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") - ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); - } - - // step at date - $qb - ->addSelect('stepHistory.step AS step') - ->addSelect('stepHistory.startDate AS stepSince') - ->leftJoin('acp.stepHistories', 'stepHistory') - ->andWhere( - $qb->expr()->andX( - $qb->expr()->lte('stepHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) - ) - ); - - // referree at date - $qb - ->addSelect('referrer_t.label AS referrer') - ->addSelect('userHistory.startDate AS referrerSince') - ->leftJoin('acp.userHistories', 'userHistory') - ->leftJoin('userHistory.user', 'referrer_t') - ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('userHistory'), - $qb->expr()->andX( - $qb->expr()->lte('userHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) - ) - ) - ); - - // location of the acp - $qb - ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') - ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') - ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') - ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') - ->leftJoin('acp.locationHistories', 'locationHistory') - ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('locationHistory'), - $qb->expr()->andX( - $qb->expr()->lte('locationHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) - ) - ) - ) - ->leftJoin( - PersonHouseholdAddress::class, - 'personAddress', - Join::WITH, - 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' - ) - ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); - - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); - - // requestor - $qb - ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') - ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') - ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') - ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') - ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') - ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); - - $qb - // scopes - ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') - // social issues - ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); - - // add parameter - $qb->setParameter('calcDate', $calcDate); - } } diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php new file mode 100644 index 000000000..8671b4977 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -0,0 +1,306 @@ +addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + ); + } + + public function getLabels($key, array $values, $data) + { + if (substr($key, 0, strlen('address_fields')) === 'address_fields') { + return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); + } + + switch ($key) { + case 'stepSince': + case 'openingDate': + case 'closingDate': + case 'referrerSince': + case 'createdAt': + case 'updatedAt': + return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); + + case 'origin': + case 'closingMotive': + case 'job': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); + }; + + case 'locationPersonName': + case 'requestorPerson': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value || null === $person = $this->personRepository->find($value)) { + return ''; + } + + return $this->personRender->renderString($person, []); + }; + + case 'requestorThirdParty': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { + return ''; + } + + return $this->thirdPartyRender->renderString($thirdparty, []); + }; + + case 'scopes': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return implode( + '|', + array_map( + fn ($s) => $this->translatableStringHelper->localize($s), + json_decode($value, true, 512, JSON_THROW_ON_ERROR) + ) + ); + }; + + case 'socialIssues': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return implode( + '|', + array_map( + fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), + json_decode($value, true, 512, JSON_THROW_ON_ERROR) + ) + ); + }; + + case 'step': + return fn ($value) => match ($value) { + '_header' => 'export.list.acp.step', + null => '', + AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), + AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), + AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), + AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), + AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), + default => $value, + }; + + case 'intensity': + return fn ($value) => match ($value) { + '_header' => 'export.list.acp.intensity', + null => '', + AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), + AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), + default => $value, + }; + + default: + return static function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + } + + public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void + { + // add the regular fields + foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); + } + + // add the field which are simple association + foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { + $qb + ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") + ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); + } + + // step at date + $qb + ->addSelect('stepHistory.step AS step') + ->addSelect('stepHistory.startDate AS stepSince') + ->leftJoin('acp.stepHistories', 'stepHistory') + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte('stepHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) + ) + ); + + // referree at date + $qb + ->addSelect('referrer_t.label AS referrer') + ->addSelect('userHistory.startDate AS referrerSince') + ->leftJoin('acp.userHistories', 'userHistory') + ->leftJoin('userHistory.user', 'referrer_t') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('userHistory'), + $qb->expr()->andX( + $qb->expr()->lte('userHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) + ) + ) + ); + + // location of the acp + $qb + ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') + ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') + ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') + ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') + ->leftJoin('acp.locationHistories', 'locationHistory') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('locationHistory'), + $qb->expr()->andX( + $qb->expr()->lte('locationHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) + ) + ) + ) + ->leftJoin( + PersonHouseholdAddress::class, + 'personAddress', + Join::WITH, + 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' + ) + ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); + + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); + + // requestor + $qb + ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') + ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') + ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') + ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') + ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') + ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); + + $qb + // scopes + ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') + // social issues + ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); + + // add parameter + $qb->setParameter('calcDate', $calcDate); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 2f7800e2b..08e51aeab 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1175,7 +1175,7 @@ export: referrerSince: Référent depuis le locationIsPerson: Parcours localisé auprès d'un usager concerné locationIsTemp: Parcours avec une localisation temporaire - acpLocationPersonName: Usager auprès duquel le parcours est localisé + locationPersonName: Usager auprès duquel le parcours est localisé locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé acpaddress_fieldscountry: Pays de l'adresse isRequestorPerson: Le demandeur est-il un usager ? From 56d9072abe971cf69f07a55a931e8becabe8fb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:33:03 +0200 Subject: [PATCH 22/87] change id, to avoid collision between ListPersonHelper and ListAccompanyingPeriodHelper --- .../Export/Helper/ListAccompanyingPeriodHelper.php | 6 ++++-- .../ChillPersonBundle/Export/Helper/ListPersonHelper.php | 7 ++++++- src/Bundle/ChillPersonBundle/translations/messages.fr.yml | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php index 8671b4977..a32d78904 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -32,7 +32,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; final readonly class ListAccompanyingPeriodHelper { public const FIELDS = [ - 'id', + 'acpId', 'step', 'stepSince', 'openingDate', @@ -219,8 +219,10 @@ final readonly class ListAccompanyingPeriodHelper public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void { + $qb->addSelect('acp.id AS acpId'); + // add the regular fields - foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); } diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php index 77a1d9c86..697019ca3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -42,7 +42,7 @@ use function strlen; class ListPersonHelper { public const FIELDS = [ - 'id', + 'personId', 'civility', 'firstName', 'lastName', @@ -124,6 +124,11 @@ class ListPersonHelper } switch ($f) { + case 'personId': + $qb->addSelect('person.id AS personId'); + + break; + case 'countryOfBirth': case 'nationality': $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 08e51aeab..0a82fc330 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -997,6 +997,8 @@ notification: Notify referrer: Notifier le référent Notify any: Notifier d'autres utilisateurs +personId: Identifiant de l'usager + export: export: acp_stats: @@ -1152,7 +1154,7 @@ export: Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres. Date of calculation for associated elements: Date de calcul des éléments associés The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date - id: Identifiant du parcours + acpId: Identifiant du parcours openingDate: Date d'ouverture du parcours closingDate: Date de fermeture du parcours closingMotive: Motif de cloture From 7f30742fc3615311500931f9d22563f228e3c752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:36:39 +0200 Subject: [PATCH 23/87] Rename ListPersonWithAccompanyingPeriod to ListPersonHavingAccompanyingPeriod --- ...ngPeriod.php => ListPersonHavingAccompanyingPeriod.php} | 7 ++++++- .../ChillPersonBundle/config/services/exports_person.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) rename src/Bundle/ChillPersonBundle/Export/Export/{ListPersonWithAccompanyingPeriod.php => ListPersonHavingAccompanyingPeriod.php} (96%) diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php similarity index 96% rename from src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php rename to src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index 370046232..f2e4de4e3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -35,7 +35,12 @@ use function count; use function in_array; use function strlen; -class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +/** + * List the persons, having an accompanying period. + * + * Details of the accompanying period are not included + */ +class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface { private ExportAddressHelper $addressHelper; diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index 64360aee9..81875fafb 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -24,7 +24,7 @@ services: tags: - { name: chill.export, alias: list_person } - Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod: + Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: autowire: true autoconfigure: true tags: From 17d2b795b41d9aee8457ad9ade199e07d1c5be60 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Fri, 7 Jul 2023 11:38:00 +0200 Subject: [PATCH 24/87] update changelog --- .changes/unreleased/DX-20230623-122408.yaml | 5 --- .../unreleased/Feature-20230623-122530.yaml | 5 --- .../unreleased/Feature-20230623-122702.yaml | 6 --- .../unreleased/Feature-20230623-124438.yaml | 5 --- .../unreleased/Fixed-20230628-170055.yaml | 6 --- .../unreleased/Fixed-20230629-124412.yaml | 6 --- .../unreleased/Fixed-20230629-231503.yaml | 5 --- .../unreleased/Fixed-20230630-171119.yaml | 5 --- .../unreleased/Fixed-20230630-171153.yaml | 5 --- .changie.yaml | 2 + CHANGELOG.md | 37 +++++++++++++++++++ .../Resources/public/chill/scss/forms.scss | 2 +- 12 files changed, 40 insertions(+), 49 deletions(-) delete mode 100644 .changes/unreleased/DX-20230623-122408.yaml delete mode 100644 .changes/unreleased/Feature-20230623-122530.yaml delete mode 100644 .changes/unreleased/Feature-20230623-122702.yaml delete mode 100644 .changes/unreleased/Feature-20230623-124438.yaml delete mode 100644 .changes/unreleased/Fixed-20230628-170055.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-124412.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-231503.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171119.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171153.yaml diff --git a/.changes/unreleased/DX-20230623-122408.yaml b/.changes/unreleased/DX-20230623-122408.yaml deleted file mode 100644 index 58dd96180..000000000 --- a/.changes/unreleased/DX-20230623-122408.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: DX -body: '[FilterOrderHelper] add entity choice and singleCheckbox' -time: 2023-06-23T12:24:08.133491895+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-122530.yaml b/.changes/unreleased/Feature-20230623-122530.yaml deleted file mode 100644 index 922750ea8..000000000 --- a/.changes/unreleased/Feature-20230623-122530.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[activity list] add filtering for activities list' -time: 2023-06-23T12:25:30.49643551+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-122702.yaml b/.changes/unreleased/Feature-20230623-122702.yaml deleted file mode 100644 index e1d1b0e1f..000000000 --- a/.changes/unreleased/Feature-20230623-122702.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: '[activity list] in person context, show also the activities from the accompanying - periods where the person participates' -time: 2023-06-23T12:27:02.159041095+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-124438.yaml b/.changes/unreleased/Feature-20230623-124438.yaml deleted file mode 100644 index bc199d3bb..000000000 --- a/.changes/unreleased/Feature-20230623-124438.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[activity list] add pagination to the list of activities' -time: 2023-06-23T12:44:38.879098862+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230628-170055.yaml b/.changes/unreleased/Fixed-20230628-170055.yaml deleted file mode 100644 index 7f9ec3028..000000000 --- a/.changes/unreleased/Fixed-20230628-170055.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: '[export] Rename label for CurrentActionFilter (on accompanying period work) - to make precision between "ouvert" and "sans date de fin"' -time: 2023-06-28T17:00:55.206937751+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-124412.yaml b/.changes/unreleased/Fixed-20230629-124412.yaml deleted file mode 100644 index 7fc3d3eb0..000000000 --- a/.changes/unreleased/Fixed-20230629-124412.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Force the db to have either a person_location or a address_location, and avoid - to have both also internally in the entity -time: 2023-06-29T12:44:12.019663991+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-231503.yaml b/.changes/unreleased/Fixed-20230629-231503.yaml deleted file mode 100644 index e021d1fda..000000000 --- a/.changes/unreleased/Fixed-20230629-231503.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] set rolling date on person age aggregator' -time: 2023-06-29T23:15:03.20841309+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171119.yaml b/.changes/unreleased/Fixed-20230630-171119.yaml deleted file mode 100644 index f3185ace2..000000000 --- a/.changes/unreleased/Fixed-20230630-171119.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] fix list when a person locating a course is without address' -time: 2023-06-30T17:11:19.454081914+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171153.yaml b/.changes/unreleased/Fixed-20230630-171153.yaml deleted file mode 100644 index c09bd93d0..000000000 --- a/.changes/unreleased/Fixed-20230630-171153.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] remove unused condition on course about duration participation' -time: 2023-06-30T17:11:53.076615549+02:00 -custom: - Issue: "" diff --git a/.changie.yaml b/.changie.yaml index 8a25ed695..cda69de65 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -30,6 +30,8 @@ kinds: auto: patch - label: DX auto: patch + - label: UX + auto: patch newlines: afterChangelogHeader: 1 beforeChangelogVersion: 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ff93556..1f51bf06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.4.0 - 2023-07-07 +### Feature +* [activity list] add filtering for activities list + + +* [activity list] in person context, show also the activities from the accompanying periods where the person participates + + +* [activity list] add pagination to the list of activities + + +* ([#118](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/118)) improve UX design for filterOrder box + + + + +### Fixed +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" + + +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity + + +* [export] set rolling date on person age aggregator + + +* [export] fix list when a person locating a course is without address + + +* [export] remove unused condition on course about duration participation + + +### DX +* [FilterOrderHelper] add entity choice and singleCheckbox + + + ## v2.3.0 - 2023-06-27 ### Feature * ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable. diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss index a517a5516..28c597bc0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss @@ -44,5 +44,5 @@ form { } .chill_filter_order { - background: $gray-100; + background: $gray-100; } \ No newline at end of file From c8146ded17b332a3566b58b7c824840259b7f7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 12:36:24 +0200 Subject: [PATCH 25/87] Feature: add a list for people with their associated accompanying course --- .../unreleased/Feature-20230707-123609.yaml | 5 + .../Export/ExportInterface.php | 2 +- ...istPersonWithAccompanyingPeriodDetails.php | 149 ++++++++++++++++++ .../Helper/ListAccompanyingPeriodHelper.php | 39 +++-- .../Export/Helper/ListPersonHelper.php | 42 ++--- .../config/services/exports_person.yaml | 20 +-- .../translations/messages.fr.yml | 10 +- 7 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 .changes/unreleased/Feature-20230707-123609.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php diff --git a/.changes/unreleased/Feature-20230707-123609.yaml b/.changes/unreleased/Feature-20230707-123609.yaml new file mode 100644 index 000000000..51ff94d4c --- /dev/null +++ b/.changes/unreleased/Feature-20230707-123609.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add a list for people with their associated course' +time: 2023-07-07T12:36:09.596469063+02:00 +custom: + Issue: "125" diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php index f357a9fdb..a11a51746 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php @@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') * @param mixed $data The data from the export's form (as defined in `buildForm`) * - * @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` + * @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` */ public function getLabels($key, array $values, $data); diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php new file mode 100644 index 000000000..295d87842 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -0,0 +1,149 @@ +add('address_date', PickRollingDateType::class, [ + 'label' => 'Data valid at this date', + 'help' => 'Data regarding center, addresses, and so on will be computed at this date', + ]); + } + public function getFormDefaultData(): array + { + return ['address_date' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_LIST]; + } + + public function getDescription() + { + return 'export.list.person_with_acp.Create a list of people having an accompaying periods with details of period, according to various filters.'; + } + + public function getGroup(): string + { + return 'Exports of persons'; + } + + public function getLabels($key, array $values, $data) + { + if (in_array($key, $this->listPersonHelper->getAllKeys(), true)) { + return $this->listPersonHelper->getLabels($key, $values, $data); + } + + return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); + } + + public function getQueryKeys($data) + { + return array_merge( + $this->listPersonHelper->getAllKeys(), + $this->listAccompanyingPeriodHelper->getQueryKeys($data), + ); + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR); + } + + public function getTitle() + { + return 'export.list.person_with_acp.List peoples having an accompanying period with period details'; + } + + public function getType() + { + return Declarations::PERSON_TYPE; + } + + /** + * param array{fields: string[], address_date: DateTimeImmutable} $data. + */ + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $centers = array_map(static fn ($el) => $el['center'], $acl); + + $qb = $this->entityManager->createQueryBuilder(); + + $qb->from(Person::class, 'person') + ->join('person.accompanyingPeriodParticipations', 'acppart') + ->join('acppart.accompanyingPeriod', 'acp') + ->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'")) + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)' + ) + )->setParameter('authorized_centers', $centers); + + $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); + $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); + + return $qb; + } + + public function requiredRole(): string + { + return PersonVoter::LISTS; + } + + public function supportsModifiers() + { + return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php index a32d78904..5fa2252cd 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -58,10 +58,10 @@ final readonly class ListAccompanyingPeriodHelper 'requestorThirdPartyId', 'scopes', 'socialIssues', - 'createdAt', - 'createdBy', - 'updatedAt', - 'updatedBy', + 'acpCreatedAt', + 'acpCreatedBy', + 'acpUpdatedAt', + 'acpUpdatedBy', ]; public function __construct( @@ -82,14 +82,14 @@ final readonly class ListAccompanyingPeriodHelper { return array_merge( ListAccompanyingPeriodHelper::FIELDS, - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'acp_address_fields') ); } public function getLabels($key, array $values, $data) { - if (substr($key, 0, strlen('address_fields')) === 'address_fields') { - return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); + if (str_starts_with($key, 'acp_address_fields')) { + return $this->addressHelper->getLabel($key, $values, $data, 'acp_address_fields'); } switch ($key) { @@ -97,8 +97,8 @@ final readonly class ListAccompanyingPeriodHelper case 'openingDate': case 'closingDate': case 'referrerSince': - case 'createdAt': - case 'updatedAt': + case 'acpCreatedAt': + case 'acpUpdatedAt': return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); case 'origin': @@ -220,14 +220,23 @@ final readonly class ListAccompanyingPeriodHelper public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void { $qb->addSelect('acp.id AS acpId'); + $qb->addSelect('acp.createdAt AS acpCreatedAt'); + $qb->addSelect('acp.updatedAt AS acpUpdatedAt'); // add the regular fields - foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity'] as $field) { $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); } // add the field which are simple association - foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { + $qb + ->leftJoin('acp.createdBy', "acp_created_by_t") + ->addSelect('acp_created_by_t.label AS acpCreatedBy'); + $qb + ->leftJoin('acp.updatedBy', "acp_updated_by_t") + ->addSelect('acp_updated_by_t.label AS acpUpdatedBy'); + + foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { $qb ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); @@ -279,13 +288,13 @@ final readonly class ListAccompanyingPeriodHelper ) ->leftJoin( PersonHouseholdAddress::class, - 'personAddress', + 'acpPersonAddress', Join::WITH, - 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' + 'locationHistory.personLocation = acpPersonAddress.person AND (acpPersonAddress.validFrom <= :calcDate AND (acpPersonAddress.validTo IS NULL OR acpPersonAddress.validTo > :calcDate))' ) - ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); + ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(acpPersonAddress.address)) = acp_address.id'); - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'acp_address_fields'); // requestor $qb diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php index 697019ca3..198794326 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Helper; +use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Repository\CenterRepositoryInterface; use Chill\MainBundle\Repository\CivilityRepositoryInterface; @@ -114,7 +115,26 @@ class ListPersonHelper } /** - * @param array|value-of[] $fields + * Those keys are the "direct" keys, which are created when we decide to use to list all the keys. + * + * This method must be used in `getKeys` instead of the `self::FIELDS` + * + * @return array + */ + public function getAllKeys(): array + { + return [ + ...array_filter( + ListPersonHelper::FIELDS, + fn (string $key) => !in_array($key, ['address_fields', 'lifecycleUpdate'], true) + ), + ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'), + ...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], + ]; + } + + /** + * @param array> $fields */ public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void { @@ -143,25 +163,7 @@ class ListPersonHelper break; case 'spokenLanguages': - $qb - ->leftJoin('person.spokenLanguages', 'spokenLanguage') - ->addSelect('AGGREGATE(spokenLanguage.id) AS spokenLanguages') - ->addGroupBy('person'); - - if (in_array('center', $fields, true)) { - $qb->addGroupBy('center'); - } - - if (in_array('address_fields', $fields, true)) { - $qb - ->addGroupBy('address_fieldsid') - ->addGroupBy('address_fieldscountry_t.id') - ->addGroupBy('address_fieldspostcode_t.id'); - } - - if (in_array('household_id', $fields, true)) { - $qb->addGroupBy('household_id'); - } + $qb->addSelect('(SELECT AGGREGATE(language.id) FROM ' . Language::class . ' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages'); break; diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index 81875fafb..43f5556cf 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -4,35 +4,27 @@ services: autowire: true ## Indicators - chill.person.export.count_person: - class: Chill\PersonBundle\Export\Export\CountPerson - autowire: true - autoconfigure: true + Chill\PersonBundle\Export\Export\CountPerson: tags: - { name: chill.export, alias: count_person } - chill.person.export.count_person_with_accompanying_course: - class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse - autowire: true - autoconfigure: true + Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse: tags: - { name: chill.export, alias: count_person_with_accompanying_course } Chill\PersonBundle\Export\Export\ListPerson: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_person } Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_person_with_acp } + Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails: + tags: + - { name: chill.export, alias: list_person_with_acp_details } + Chill\PersonBundle\Export\Export\ListAccompanyingPeriod: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_acp } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 0a82fc330..1159b2c49 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1148,7 +1148,9 @@ export: list: person_with_acp: List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement + List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager + Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste. acp: List of accompanying periods: Liste des parcours d'accompagnements Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres. @@ -1162,14 +1164,14 @@ export: confidential: Confidentiel emergency: Urgent intensity: Intensité - createdAt: Créé le - updatedAt: Dernière mise à jour le + acpCreatedAt: Créé le + acpUpdatedAt: Dernière mise à jour le acpOrigin: Origine du parcours origin: Origine du parcours acpClosingMotive: Motif de fermeture acpJob: Métier du parcours - createdBy: Créé par - updatedBy: Dernière modification par + acpCreatedBy: Créé par + acpUpdatedBy: Dernière modification par administrativeLocation: Location administrative step: Etape stepSince: Dernière modification de l'étape From 63f9bd554897e1363d0b321c9e80c28e2b7745bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 12:42:32 +0200 Subject: [PATCH 26/87] [export] Add ordering by person''s lastname or course opening date in list which concerns accompanying course or people --- .changes/unreleased/Feature-20230707-124132.yaml | 6 ++++++ .../Export/Export/ListAccompanyingPeriod.php | 5 +++++ .../Export/Export/ListPersonHavingAccompanyingPeriod.php | 5 +++++ .../Export/ListPersonWithAccompanyingPeriodDetails.php | 6 ++++++ 4 files changed, 22 insertions(+) create mode 100644 .changes/unreleased/Feature-20230707-124132.yaml diff --git a/.changes/unreleased/Feature-20230707-124132.yaml b/.changes/unreleased/Feature-20230707-124132.yaml new file mode 100644 index 000000000..4ad93ad22 --- /dev/null +++ b/.changes/unreleased/Feature-20230707-124132.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: '[export] Add ordering by person''s lastname or course opening date in list + which concerns accompanying course or peoples' +time: 2023-07-07T12:41:32.112725962+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php index 3f7821bbc..ab9c0db2f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -133,6 +133,11 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); + $qb + ->addOrderBy('acp.openingDate') + ->addOrderBy('acp.closingDate') + ->addOrderBy('acp.id'); + return $qb; } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index f2e4de4e3..408d0b3af 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -190,6 +190,11 @@ class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterf $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); + $qb + ->addOrderBy('person.lastName') + ->addOrderBy('person.firstName') + ->addOrderBy('person.id'); + return $qb; } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php index 295d87842..ddb16bb2d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -134,6 +134,12 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); + $qb + ->addOrderBy('person.lastName') + ->addOrderBy('person.firstName') + ->addOrderBy('person.id') + ->addOrderBy('acp.id'); + return $qb; } From 20e64e87680d102c686000f1c35d0139d00eecdf Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Fri, 7 Jul 2023 15:41:29 +0200 Subject: [PATCH 27/87] test filterOrder in an accordion --- .../views/FilterOrder/base.html.twig | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index b2673b60c..f642c82aa 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -1,92 +1,102 @@ {{ form_start(form) }} - {% set btnSubmit = 0 %} -
-
- {% if form.vars.has_search_box %} -
-
- {{ form_widget(form.q) }} - -
-
- {% endif %} -
- {% if form.dateRanges is defined %} - {% set btnSubmit = 1 %} - {% if form.dateRanges|length > 0 %} - {% for dateRangeName, _o in form.dateRanges %} -
- {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} - {{ form_label(form.dateRanges[dateRangeName])}} - {% else %} -
{{ 'activity_filter.By date'|trans }}
- {% endif %} -
-
- {{ 'chill_calendar.From'|trans }} - {{ form_widget(form.dateRanges[dateRangeName]['from']) }} - {{ 'chill_calendar.To'|trans }} - {{ form_widget(form.dateRanges[dateRangeName]['to']) }} -
+
+

+ +

+
+ {% set btnSubmit = 0 %} +
+
+ {% if form.vars.has_search_box %} +
+
+ {{ form_widget(form.q) }} +
- {% endfor %} + {% endif %} +
+ {% if form.dateRanges is defined %} + {% set btnSubmit = 1 %} + {% if form.dateRanges|length > 0 %} + {% for dateRangeName, _o in form.dateRanges %} +
+ {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} + {{ form_label(form.dateRanges[dateRangeName])}} + {% else %} +
{{ 'activity_filter.By date'|trans }}
+ {% endif %} +
+
+ {{ 'chill_calendar.From'|trans }} + {{ form_widget(form.dateRanges[dateRangeName]['from']) }} + {{ 'chill_calendar.To'|trans }} + {{ form_widget(form.dateRanges[dateRangeName]['to']) }} +
+
+
+ {% endfor %} + {% endif %} {% endif %} - {% endif %} - {% if form.checkboxes is defined %} - {% set btnSubmit = 1 %} - {% if form.checkboxes|length > 0 %} - {% for checkbox_name, options in form.checkboxes %} + {% if form.checkboxes is defined %} + {% set btnSubmit = 1 %} + {% if form.checkboxes|length > 0 %} + {% for checkbox_name, options in form.checkboxes %} +
+
{{ 'activity_filter.By'|trans }}
+
+ {% for c in form['checkboxes'][checkbox_name].children %} + {{ form_widget(c) }} + {{ form_label(c) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.entity_choices is defined %} + {% set btnSubmit = 1 %} + {% if form.entity_choices |length > 0 %} + {% for checkbox_name, options in form.entity_choices %} +
+ {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} + {{ form_label(form.entity_choices[checkbox_name])}} + {% endif %} +
+ {% for c in form['entity_choices'][checkbox_name].children %} + {{ form_widget(c) }} + {{ form_label(c) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.single_checkboxes is defined %} + {% set btnSubmit = 1 %} + {% for name, _o in form.single_checkboxes %}
{{ 'activity_filter.By'|trans }}
- {% for c in form['checkboxes'][checkbox_name].children %} - {{ form_widget(c) }} - {{ form_label(c) }} - {% endfor %} + {{ form_widget(form.single_checkboxes[name]) }}
{% endfor %} {% endif %} - {% endif %} - {% if form.entity_choices is defined %} - {% set btnSubmit = 1 %} - {% if form.entity_choices |length > 0 %} - {% for checkbox_name, options in form.entity_choices %} -
- {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} - {{ form_label(form.entity_choices[checkbox_name])}} - {% endif %} -
- {% for c in form['entity_choices'][checkbox_name].children %} - {{ form_widget(c) }} - {{ form_label(c) }} - {% endfor %} -
-
- {% endfor %} - {% endif %} - {% endif %} - {% if form.single_checkboxes is defined %} - {% set btnSubmit = 1 %} - {% for name, _o in form.single_checkboxes %} + + {% if btnSubmit == 1 %}
-
{{ 'activity_filter.By'|trans }}
-
- {{ form_widget(form.single_checkboxes[name]) }} -
+
- {% endfor %} - {% endif %} - - {% if btnSubmit == 1 %} -
- -
- {% endif %} + {% endif %} +
+
- {% for k,v in otherParameters %} - - {% endfor %} +{% for k,v in otherParameters %} + +{% endfor %} {{ form_end(form) }} + From 6bdb3e969538bc3cf0ad3458d5015d3ab180626e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 21:49:36 +0200 Subject: [PATCH 28/87] fix typo which prevent to apply a filter on activity types --- .../ChillActivityBundle/Controller/ActivityController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 9b6e69bf0..1e911ff08 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -259,7 +259,7 @@ final class ActivityController extends AbstractController $filterArgs = [ 'my_activities' => $filter->getSingleCheckboxData('my_activities'), - 'types' => $filter->hasEntityChoice('activity_type') ? $filter->getEntityChoiceData('activity_types') : [], + 'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [], 'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [], 'before' => $filter->getDateRangeData('activity_date')['to'], 'after' => $filter->getDateRangeData('activity_date')['from'], From 39896ea6e26e3cb2392830b2e18c9bb8f396cefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 10 Jul 2023 15:26:54 +0200 Subject: [PATCH 29/87] [FilterOrder] add a method to get all the active filters --- .../Form/Type/Listing/FilterOrderType.php | 25 +++--- .../views/FilterOrder/base.html.twig | 13 ++- .../Templating/Listing/FilterOrderHelper.php | 79 ++++++++++++++++--- .../Listing/FilterOrderHelperBuilder.php | 10 ++- .../Listing/FilterOrderHelperFactory.php | 8 +- .../Listing/FilterOrderPositionEnum.php | 12 +++ .../translations/messages+intl-icu.fr.yaml | 5 ++ 7 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index 1f373400c..d16ce3813 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -47,16 +47,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]); foreach ($helper->getCheckboxes() as $name => $c) { - $choices = array_combine( - array_map(static function ($c, $t) { - if (null !== $t) { - return $t; - } - - return $c; - }, $c['choices'], $c['trans']), - $c['choices'] - ); + $choices = self::buildCheckboxChoices($c['choices'], $c['trans']); $checkboxesBuilder->add($name, ChoiceType::class, [ 'choices' => $choices, @@ -125,6 +116,20 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType } } + public static function buildCheckboxChoices(array $choices, array $trans = []): array + { + return array_combine( + array_map(static function ($c, $t) { + if (null !== $t) { + return $t; + } + + return $c; + }, $choices, $trans), + $choices + ); + } + public function buildView(FormView $view, FormInterface $form, array $options) { /** @var FilterOrderHelper $helper */ diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index f642c82aa..4de7604cf 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -85,7 +85,7 @@
{% endfor %} {% endif %} - + {% if btnSubmit == 1 %}
@@ -93,6 +93,17 @@ {% endif %}
+ {% set active = helper.getActiveFilters() %} + {% if active|length > 0 %} +
+ {% for f in active %} + {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + {% endfor %} +
+ {% endif %} +
+ +
{% for k,v in otherParameters %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index c0ef1cd89..a038dfd71 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -13,16 +13,19 @@ namespace Chill\MainBundle\Templating\Listing; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; use function count; -class FilterOrderHelper +final class FilterOrderHelper { private array $checkboxes = []; @@ -33,16 +36,12 @@ class FilterOrderHelper private array $dateRanges = []; - private FormFactoryInterface $formFactory; - public const FORM_NAME = 'f'; private array $formOptions = []; private string $formType = FilterOrderType::class; - private RequestStack $requestStack; - private ?array $searchBoxFields = null; private ?array $submitted = null; @@ -52,12 +51,13 @@ class FilterOrderHelper */ private array $entityChoices = []; + public function __construct( - FormFactoryInterface $formFactory, - RequestStack $requestStack + private readonly FormFactoryInterface $formFactory, + private readonly RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { - $this->formFactory = $formFactory; - $this->requestStack = $requestStack; } public function addSingleCheckbox(string $name, string $label): self @@ -199,6 +199,63 @@ class FilterOrderHelper return $this; } + /** + * Return all the data required to display the active filters + * + * @return array + */ + public function getActiveFilters(): array + { + $result = []; + + if ($this->hasSearchBox() && '' !== $this->getQueryString()) { + $result[] = ['label' => '', 'value' => $this->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; + } + + foreach ($this->dateRanges as $name => ['label' => $label]) { + $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string) $label]; + + if (null !== ($from = $this->getDateRangeData($name)['from'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; + } + if (null !== ($to = $this->getDateRangeData($name)['to'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; + } + } + + foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans, 'options' => $options]) { + foreach ($this->getCheckboxData($name) as $keyChoice) { + $result[] = ['value' => $choices['keyChoice'], 'label' => $options['label'], 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + } + } + + foreach ($this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { + foreach ($this->getEntityChoiceData($name) as $selected) { + if (is_callable($options['choice_label'])) { + $value = call_user_func($options['choice_label'], $selected); + } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { + $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); + } else { + if (!$selected instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + } + + $value = (string) $selected; + } + + $result[] = ['value' => $value, 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + } + } + + foreach ($this->singleCheckbox as $name => ['label' => $label]) { + if (true === $this->getSingleCheckboxData($name)) { + $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; + } + } + + return $result; + } + private function getDefaultData(): array { $r = [ diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index e176e27c6..dfa361f6e 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Listing; use DateTimeImmutable; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FilterOrderHelperBuilder { @@ -39,7 +41,9 @@ class FilterOrderHelperBuilder public function __construct( FormFactoryInterface $formFactory, - RequestStack $requestStack + RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -87,7 +91,9 @@ class FilterOrderHelperBuilder { $helper = new FilterOrderHelper( $this->formFactory, - $this->requestStack + $this->requestStack, + $this->translator, + $this->propertyAccessor ); $helper->setSearchBox($this->searchBoxFields); diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php index c88c71af5..3fbb2864d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface { @@ -22,7 +24,9 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function __construct( FormFactoryInterface $formFactory, - RequestStack $requestStack + RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -30,6 +34,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function create(string $context, ?array $options = []): FilterOrderHelperBuilder { - return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack); + return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack, $this->translator, $this->propertyAccessor); } } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php new file mode 100644 index 000000000..cda8119f5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -0,0 +1,12 @@ + Date: Mon, 10 Jul 2023 15:39:00 +0200 Subject: [PATCH 30/87] [filterOrder] fix error in method getActiveFilters when dealing with entityChoice with incorrect number of translation --- .../Templating/Listing/FilterOrderHelper.php | 13 ++++++++----- .../Templating/Listing/FilterOrderPositionEnum.php | 9 +++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index a038dfd71..701238208 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -84,9 +84,11 @@ final class FilterOrderHelper public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { - $missing = count($choices) - count($trans) - 1; + $missing = count($choices) - count($trans); + $this->checkboxes[$name] = [ - 'choices' => $choices, 'default' => $default, + 'choices' => $choices, + 'default' => $default, 'trans' => array_merge( $trans, 0 < $missing ? @@ -223,9 +225,10 @@ final class FilterOrderHelper } } - foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans, 'options' => $options]) { + foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { + $translatedChoice = array_combine($choices, [...$trans]); foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $choices['keyChoice'], 'label' => $options['label'], 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + $result[] = ['value' => $translatedChoice[$keyChoice], 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; } } @@ -237,7 +240,7 @@ final class FilterOrderHelper $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); } else { if (!$selected instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); } $value = (string) $selected; diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php index cda8119f5..09e8d39aa 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -1,5 +1,14 @@ Date: Mon, 10 Jul 2023 15:55:05 +0200 Subject: [PATCH 31/87] render active filters like pills --- .../Resources/views/FilterOrder/base.html.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 4de7604cf..7a05a2f80 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -95,9 +95,9 @@
{% set active = helper.getActiveFilters() %} {% if active|length > 0 %} -
+
{% for f in active %} - {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} {% endfor %}
{% endif %} From 0d365e16e58df1614b132fc4b1fa2dc655458e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 10 Jul 2023 15:59:17 +0200 Subject: [PATCH 32/87] add missing translations --- .../ChillMainBundle/Templating/Listing/FilterOrderHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 701238208..2b24ffa0d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -228,7 +228,7 @@ final class FilterOrderHelper foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { $translatedChoice = array_combine($choices, [...$trans]); foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $translatedChoice[$keyChoice], 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; } } @@ -246,7 +246,7 @@ final class FilterOrderHelper $value = (string) $selected; } - $result[] = ['value' => $value, 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; } } From bf93c1ddb21f2bcbb28d42094b4f0d7c4cc78ef5 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Tue, 11 Jul 2023 14:06:10 +0200 Subject: [PATCH 33/87] fix label color in active filters pills --- .../translations/messages.fr.yml | 3 --- .../Form/Type/Listing/FilterOrderType.php | 2 +- .../Resources/views/FilterOrder/base.html.twig | 16 ++++++++++++---- .../translations/messages+intl-icu.fr.yaml | 4 ++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 3099e99b0..c53a04f31 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -96,9 +96,6 @@ activity_filter: My activities: Mes échanges (où j'interviens) Types: Par type d'échange Jobs: Par métier impliqué - By: Filtrer par - Search: Chercher dans la liste - By date: Filtrer par date #timeline '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index d16ce3813..f22b6bfba 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -39,7 +39,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType 'label' => false, 'required' => false, 'attr' => [ - 'placeholder' => 'activity_filter.Search', + 'placeholder' => 'filter_order.Search', ] ]); } diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 7a05a2f80..0dcc4ce3f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -26,7 +26,7 @@ {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} {{ form_label(form.dateRanges[dateRangeName])}} {% else %} -
{{ 'activity_filter.By date'|trans }}
+
{{ 'filter_order.By date'|trans }}
{% endif %}
@@ -45,7 +45,7 @@ {% if form.checkboxes|length > 0 %} {% for checkbox_name, options in form.checkboxes %}
-
{{ 'activity_filter.By'|trans }}
+
{{ 'filter_order.By'|trans }}
{% for c in form['checkboxes'][checkbox_name].children %} {{ form_widget(c) }} @@ -78,7 +78,7 @@ {% set btnSubmit = 1 %} {% for name, _o in form.single_checkboxes %}
-
{{ 'activity_filter.By'|trans }}
+
{{ 'filter_order.By'|trans }}
{{ form_widget(form.single_checkboxes[name]) }}
@@ -97,7 +97,15 @@ {% if active|length > 0 %}
{% for f in active %} - {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + + {%- if f.label != '' %} + {{ f.label|trans }} : + {% endif -%} + {%- if f.position == 'search_box' and f.value is not null %} + {{ 'filter_order.search_box'|trans ~ ' :' }} + {% endif -%} + {{ f.value}}{# + #} {% endfor %}
{% endif %} diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index e1dc89dc2..96b2edd98 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -59,3 +59,7 @@ filter_order: by_date: From: Depuis le {from_date, date, long} To: Jusqu'au {to_date, date, long} + By: Filtrer par + Search: Chercher dans la liste + By date: Filtrer par date + search_box: Filtrer par contenu From 88114e3ba69fbd97a27eb850600acc78e27f526a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 14:17:02 +0200 Subject: [PATCH 34/87] Fixed: [filterOrder] refactor active filter helper to a dedicated class and fix loading of multiple entity choices --- .../views/FilterOrder/base.html.twig | 1 - .../FilterOrderGetActiveFilterHelper.php | 84 +++++++++++++++++++ .../Templating/Listing/FilterOrderHelper.php | 70 +--------------- .../Listing/FilterOrderHelperBuilder.php | 4 - .../Listing/FilterOrderHelperFactory.php | 4 +- .../Templating/Listing/Templating.php | 2 + 6 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 0dcc4ce3f..b517eb154 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -93,7 +93,6 @@ {% endif %}
- {% set active = helper.getActiveFilters() %} {% if active|length > 0 %}
{% for f in active %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php new file mode 100644 index 000000000..6b204e552 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php @@ -0,0 +1,84 @@ + + */ + public function getActiveFilters(FilterOrderHelper $filterOrderHelper): array + { + $result = []; + + if ($filterOrderHelper->hasSearchBox() && '' !== $filterOrderHelper->getQueryString()) { + $result[] = ['label' => '', 'value' => $filterOrderHelper->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; + } + + foreach ($filterOrderHelper->getDateRanges() as $name => ['label' => $label]) { + $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string)$label]; + + if (null !== ($from = $filterOrderHelper->getDateRangeData($name)['from'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; + } + if (null !== ($to = $filterOrderHelper->getDateRangeData($name)['to'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; + } + } + + foreach ($filterOrderHelper->getCheckboxes() as $name => ['choices' => $choices, 'trans' => $trans]) { + $translatedChoice = array_combine($choices, [...$trans]); + foreach ($filterOrderHelper->getCheckboxData($name) as $keyChoice) { + $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + } + } + + foreach ($filterOrderHelper->getEntityChoices() as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { + foreach ($filterOrderHelper->getEntityChoiceData($name) as $selected) { + if (is_callable($options['choice_label'])) { + $value = call_user_func($options['choice_label'], $selected); + } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { + $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); + } else { + if (!$selected instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + } + + $value = (string)$selected; + } + + $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + } + } + + foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) { + if (true === $filterOrderHelper->getSingleCheckboxData($name)) { + $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; + } + } + + return $result; + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 2b24ffa0d..84939a052 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -55,8 +55,6 @@ final class FilterOrderHelper public function __construct( private readonly FormFactoryInterface $formFactory, private readonly RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { } @@ -84,16 +82,14 @@ final class FilterOrderHelper public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { - $missing = count($choices) - count($trans); + if ([] === $trans) { + $trans = $choices; + } $this->checkboxes[$name] = [ 'choices' => $choices, 'default' => $default, - 'trans' => array_merge( - $trans, - 0 < $missing ? - array_fill(0, $missing, null) : [] - ), + 'trans' => $trans, ...$options, ]; @@ -201,64 +197,6 @@ final class FilterOrderHelper return $this; } - /** - * Return all the data required to display the active filters - * - * @return array - */ - public function getActiveFilters(): array - { - $result = []; - - if ($this->hasSearchBox() && '' !== $this->getQueryString()) { - $result[] = ['label' => '', 'value' => $this->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; - } - - foreach ($this->dateRanges as $name => ['label' => $label]) { - $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string) $label]; - - if (null !== ($from = $this->getDateRangeData($name)['from'] ?? null)) { - $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; - } - if (null !== ($to = $this->getDateRangeData($name)['to'] ?? null)) { - $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; - } - } - - foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { - $translatedChoice = array_combine($choices, [...$trans]); - foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; - } - } - - foreach ($this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { - foreach ($this->getEntityChoiceData($name) as $selected) { - if (is_callable($options['choice_label'])) { - $value = call_user_func($options['choice_label'], $selected); - } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { - $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); - } else { - if (!$selected instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); - } - - $value = (string) $selected; - } - - $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; - } - } - - foreach ($this->singleCheckbox as $name => ['label' => $label]) { - if (true === $this->getSingleCheckboxData($name)) { - $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; - } - } - - return $result; - } - private function getDefaultData(): array { $r = [ diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index dfa361f6e..f2bded220 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -42,8 +42,6 @@ class FilterOrderHelperBuilder public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -92,8 +90,6 @@ class FilterOrderHelperBuilder $helper = new FilterOrderHelper( $this->formFactory, $this->requestStack, - $this->translator, - $this->propertyAccessor ); $helper->setSearchBox($this->searchBoxFields); diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php index 3fbb2864d..6665750dd 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php @@ -25,8 +25,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -34,6 +32,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function create(string $context, ?array $options = []): FilterOrderHelperBuilder { - return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack, $this->translator, $this->propertyAccessor); + return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack); } } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php index b91cd86e8..2d32813cb 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php @@ -24,6 +24,7 @@ class Templating extends AbstractExtension { public function __construct( private readonly RequestStack $requestStack, + private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper, ) { } @@ -68,6 +69,7 @@ class Templating extends AbstractExtension return $environment->render($template, [ 'helper' => $helper, + 'active' => $this->filterOrderGetActiveFilterHelper->getActiveFilters($helper), 'form' => $helper->buildForm()->createView(), 'options' => $options, 'otherParameters' => $otherParameters, From 6065680e1e0bc71c19dd05ef158d301324f61805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 15:01:32 +0200 Subject: [PATCH 35/87] Feature: [export] allow to group activities by location --- .../unreleased/Feature-20230711-150055.yaml | 5 ++ .../Aggregator/ActivityLocationAggregator.php | 80 +++++++++++++++++++ .../config/services/export.yaml | 4 + .../translations/messages.fr.yml | 3 + 4 files changed, 92 insertions(+) create mode 100644 .changes/unreleased/Feature-20230711-150055.yaml create mode 100644 src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php diff --git a/.changes/unreleased/Feature-20230711-150055.yaml b/.changes/unreleased/Feature-20230711-150055.yaml new file mode 100644 index 000000000..ecee61b49 --- /dev/null +++ b/.changes/unreleased/Feature-20230711-150055.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[Export] allow to group activities by localisation' +time: 2023-07-11T15:00:55.770070399+02:00 +custom: + Issue: "128" diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php new file mode 100644 index 000000000..9103943e4 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php @@ -0,0 +1,80 @@ +getAllAliases(), true)) { + $qb->leftJoin('activity.location', 'actloc'); + } + $qb->addSelect(sprintf('actloc.name AS %s', self::KEY)); + $qb->addGroupBy(self::KEY); + } + + public function applyOn(): string + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + // no form required for this aggregator + } + public function getFormDefaultData(): array + { + return []; + } + + public function getLabels($key, array $values, $data): Closure + { + return function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.activity.by_location.Activity Location'; + } + + if (null === $value || '' === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return [self::KEY]; + } + + public function getTitle() + { + return 'export.aggregator.activity.by_location.Title'; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 09817d80e..03285c416 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -144,6 +144,10 @@ services: tags: - { name: chill.export_aggregator, alias: activity_common_type_aggregator } + Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_common_location_aggregator } + chill.activity.export.user_aggregator: class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator tags: diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index abef160d3..037c24f3f 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -372,6 +372,9 @@ export: is sent: envoyé is received: reçu Group activity by sentreceived: Grouper les échanges par envoyé / reçu + by_location: + Activity Location: Localisation de l'échange + Title: Grouper les échanges par localisation de l'échange generic_doc: filter: From 2882038efcd4be2b65db67df7cb4f2498fcb0f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 16:00:40 +0200 Subject: [PATCH 36/87] [export] Add a filter "filter course having an activity between two dates" --- .../unreleased/Feature-20230711-155929.yaml | 5 ++ ...PeriodHavingActivityBetweenDatesFilter.php | 90 +++++++++++++++++++ .../config/services/export.yaml | 4 + .../translations/messages+intl-icu.fr.yml | 5 ++ .../translations/messages.fr.yml | 6 ++ 5 files changed, 110 insertions(+) create mode 100644 .changes/unreleased/Feature-20230711-155929.yaml create mode 100644 src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php create mode 100644 src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml diff --git a/.changes/unreleased/Feature-20230711-155929.yaml b/.changes/unreleased/Feature-20230711-155929.yaml new file mode 100644 index 000000000..329bbb677 --- /dev/null +++ b/.changes/unreleased/Feature-20230711-155929.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add a filter "filter course having an activity between two dates"' +time: 2023-07-11T15:59:29.065329834+02:00 +custom: + Issue: "129" diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php new file mode 100644 index 000000000..27e012d0b --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php @@ -0,0 +1,90 @@ +add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity before' + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY) + ]; + } + + public function describeAction($data, $format = 'string') + { + return [ + 'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to', + [ + 'from' => $this->rollingDateConverter->convert($data['start_date']), + 'to' => $this->rollingDateConverter->convert($data['end_date']), + ] + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $alias = 'act_period_having_act_betw_date_alias'; + $from = 'act_period_having_act_betw_date_start'; + $to = 'act_period_having_act_betw_date_end'; + + $qb->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . Activity::class . " {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp" + ) + ); + + $qb + ->setParameter($from, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($to, $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn() + { + return \Chill\PersonBundle\Export\Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 09817d80e..932985083 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -135,6 +135,10 @@ services: tags: - { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' } + Chill\ActivityBundle\Export\Filter\ACPFilters\PeriodHavingActivityBetweenDatesFilter: + tags: + - { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' } + ## Aggregators Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator: tags: diff --git a/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml new file mode 100644 index 000000000..ab3b963ab --- /dev/null +++ b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml @@ -0,0 +1,5 @@ +export: + filter: + activity: + course_having_activity_between_date: + Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short} diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index abef160d3..551a63d27 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -365,6 +365,12 @@ export: by_usersscope: Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant 'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%' + course_having_activity_between_date: + Title: Filtre les parcours ayant reçu un échange entre deux dates + Receiving an activity after: Ayant reçu un échange après le + Receiving an activity before: Ayant reçu un échange avant le + + aggregator: activity: by_sent_received: From edd66f6a6cf3622dd82a8535a9f4a6272cc9bb1d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 12 Jul 2023 09:04:15 +0200 Subject: [PATCH 37/87] FIX [budget][templates] reimplement display of all calculator results --- .../Resources/views/Budget/_macros.html.twig | 33 ++++++++++++++----- .../Resources/views/Person/index.html.twig | 2 +- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig index dfa286af4..a1fee19ce 100644 --- a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig +++ b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig @@ -1,11 +1,12 @@ -{% macro table_elements(elements, family) %} +{% macro table_elements(elements, type) %} + - - - - + + + + @@ -38,17 +39,17 @@
    {% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
  • - +
  • {% endif %} {% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
  • - +
  • {% endif %} {% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
  • - +
  • {% endif %}
@@ -69,7 +70,7 @@
{{ 'Budget element type'|trans }}{{ 'Amount'|trans }}{{ 'Validity period'|trans }} {{ 'Budget element type'|trans }}{{ 'Amount'|trans }}{{ 'Validity period'|trans }} 
{% endmacro %} -{% macro table_results(actualCharges, actualResources) %} +{% macro table_results(actualCharges, actualResources, results) %} {% set totalCharges = 0 %} {% for c in actualCharges %} @@ -97,6 +98,20 @@ {{ result|format_currency('EUR') }} + {% for result in results %} + + {{ result.label }} + + {% if result.type == 'currency' %} + {{ result.result|format_currency('EUR') }} + {% elseif result.type == 'percentage' %} + {{ result.result|round(2, 'ceil') ~ '%' }} + {% else %} + {{ result.result|round(2, 'common') }} + {% endif %} + + + {% endfor %} {% endmacro %} diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig index 18d04b889..aba564206 100644 --- a/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig +++ b/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig @@ -25,7 +25,7 @@

{{ 'Budget calculator'|trans }}

- {{ table_results(charges, resources) }} + {{ table_results(charges, resources, results) }}
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %} From f7d385eba1877756c34ed64857d0e847aedccf9d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 12 Jul 2023 09:06:08 +0200 Subject: [PATCH 38/87] DX add changie --- .changes/unreleased/Fixed-20230712-090514.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Fixed-20230712-090514.yaml diff --git a/.changes/unreleased/Fixed-20230712-090514.yaml b/.changes/unreleased/Fixed-20230712-090514.yaml new file mode 100644 index 000000000..51a8b9317 --- /dev/null +++ b/.changes/unreleased/Fixed-20230712-090514.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: reimplement the visualization of all calculator results (specific to AMLI) +time: 2023-07-12T09:05:14.416268226+02:00 +custom: + Issue: "" From e38b369149dc92c6db0a36963c562c2976088540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 12 Jul 2023 10:26:22 +0200 Subject: [PATCH 39/87] [cron-job] add a new "lastExecution" data on CronJobExecution entity This column will store the results of the last execution --- .../Entity/CronJobExecution.php | 18 +++++++++- .../migrations/Version20230711152947.php | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20230711152947.php diff --git a/src/Bundle/ChillMainBundle/Entity/CronJobExecution.php b/src/Bundle/ChillMainBundle/Entity/CronJobExecution.php index 0cacffac9..2883055fc 100644 --- a/src/Bundle/ChillMainBundle/Entity/CronJobExecution.php +++ b/src/Bundle/ChillMainBundle/Entity/CronJobExecution.php @@ -31,7 +31,6 @@ class CronJobExecution private string $key; /** - * @var DateTimeImmutable * @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null}) */ private ?DateTimeImmutable $lastEnd = null; @@ -46,6 +45,11 @@ class CronJobExecution */ private ?int $lastStatus = null; + /** + * @ORM\Column(type="json", options={"default": "'{}'::jsonb", "jsonb": true}) + */ + private array $lastExecutionData = []; + public function __construct(string $key) { $this->key = $key; @@ -92,4 +96,16 @@ class CronJobExecution return $this; } + + public function getLastExecutionData(): array + { + return $this->lastExecutionData; + } + + public function setLastExecutionData(array $lastExecutionData): CronJobExecution + { + $this->lastExecutionData = $lastExecutionData; + + return $this; + } } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230711152947.php b/src/Bundle/ChillMainBundle/migrations/Version20230711152947.php new file mode 100644 index 000000000..ed804473e --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20230711152947.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_main_cronjob_execution ADD lastExecutionData JSONB DEFAULT \'{}\'::jsonb NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_cronjob_execution DROP COLUMN lastExecutionData'); + } +} From a2e705bd92e085007fb0313b163f194497bcb5bf Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:38:11 +0200 Subject: [PATCH 40/87] fixed: error with parent joins in thirdparty api search query --- .../Controller/resquery.bad.sql | 21 +++++++++++++++++++ .../Controller/resquery.fixed.sql | 21 +++++++++++++++++++ .../Search/ThirdPartyApiSearch.php | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.bad.sql create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql new file mode 100644 index 000000000..1033ec28c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql @@ -0,0 +1,21 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id +LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id +WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql new file mode 100644 index 000000000..dbb55f187 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql @@ -0,0 +1,21 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id +LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id +WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php index 86c0fa9db..42b98622f 100644 --- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php +++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php @@ -75,7 +75,7 @@ class ThirdPartyApiSearch implements SearchApiInterface LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id - LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id') + LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id') ->andWhereClause('tparty.active IS TRUE'); $strs = explode(' ', $pattern); From f3829d3390ae7e22939f02a6bb71db305ebe060e Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:50:17 +0200 Subject: [PATCH 41/87] adapt query to simplify join clauses (lightly improve perfs) --- .../Controller/resquery.good.sql | 19 +++++++++++++++++++ .../Search/ThirdPartyApiSearch.php | 10 +++------- 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.good.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql new file mode 100644 index 000000000..5877b04f8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql @@ -0,0 +1,19 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +WHERE (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC, tparty.id ASC LIMIT 500 OFFSET 0; diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php index 42b98622f..bb5303143 100644 --- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php +++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php @@ -71,12 +71,9 @@ class ThirdPartyApiSearch implements SearchApiInterface ->setSelectKey('tparty') ->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)") ->setFromClause('chill_3party.third_party AS tparty - LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id - LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id - LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id - LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id') - ->andWhereClause('tparty.active IS TRUE'); + LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) + LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id'); $strs = explode(' ', $pattern); $wheres = []; @@ -102,8 +99,7 @@ class ThirdPartyApiSearch implements SearchApiInterface (parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int ) + " . // take postcode label into account, but lower than the canonicalized field - "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + " . - "COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)"; + "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)"; $pertinenceArgs[] = [$str, $str, $str, $str, $str, $str]; } } From efee2d8b44369e6546d13afa19d58b8a43d086db Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:53:12 +0200 Subject: [PATCH 42/87] cleaning --- .../Controller/resquery.bad.sql | 21 ------------------- .../Controller/resquery.fixed.sql | 21 ------------------- .../Controller/resquery.good.sql | 19 ----------------- 3 files changed, 61 deletions(-) delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.bad.sql delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.good.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql deleted file mode 100644 index 1033ec28c..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql +++ /dev/null @@ -1,21 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id -LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id -WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql deleted file mode 100644 index dbb55f187..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql +++ /dev/null @@ -1,21 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id -LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id -WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql deleted file mode 100644 index 5877b04f8..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -WHERE (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC, tparty.id ASC LIMIT 500 OFFSET 0; From 3f66e1a862416658a3afce675dbc1cb29440fb24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 12 Jul 2023 11:36:26 +0200 Subject: [PATCH 43/87] [cron-job] allow a cronjob to pass data from one execution to another When a cronjob is executed, it may return an array of data. This data will be passed as parameter on the next execution --- .changes/unreleased/DX-20230712-113603.yaml | 6 ++ .../ChillMainBundle/Cron/CronJobInterface.php | 10 ++- .../ChillMainBundle/Cron/CronManager.php | 25 +++++- ...eographicalUnitMaterializedViewCronJob.php | 4 +- .../Cron/CronJobDatabaseInteractionTest.php | 87 +++++++++++++++++++ .../Tests/Cron/CronManagerTest.php | 12 +-- .../AccompanyingPeriodStepChangeCronjob.php | 4 +- 7 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 .changes/unreleased/DX-20230712-113603.yaml create mode 100644 src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php diff --git a/.changes/unreleased/DX-20230712-113603.yaml b/.changes/unreleased/DX-20230712-113603.yaml new file mode 100644 index 000000000..518ac3ca9 --- /dev/null +++ b/.changes/unreleased/DX-20230712-113603.yaml @@ -0,0 +1,6 @@ +kind: DX +body: '[cronjob] when a cronjob is executed, it may return an array of data that will + be passed as argument on the next execution' +time: 2023-07-12T11:36:03.813179067+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php index 4e1ca9ff6..69edf8464 100644 --- a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php +++ b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php @@ -19,5 +19,13 @@ interface CronJobInterface public function getKey(): string; - public function run(): void; + /** + * Execute the cronjob + * + * If data is returned, this data is passed as argument on the next execution + * + * @param array $lastExecutionData the data which was returned from the previous execution + * @return array|null optionally return an array with the same data than the previous execution + */ + public function run(array $lastExecutionData): null|array; } diff --git a/src/Bundle/ChillMainBundle/Cron/CronManager.php b/src/Bundle/ChillMainBundle/Cron/CronManager.php index f69dcba76..a3e82a170 100644 --- a/src/Bundle/ChillMainBundle/Cron/CronManager.php +++ b/src/Bundle/ChillMainBundle/Cron/CronManager.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Cron; use Chill\MainBundle\Entity\CronJobExecution; use Chill\MainBundle\Repository\CronJobExecutionRepositoryInterface; use DateTimeImmutable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Exception; use Psr\Log\LoggerInterface; @@ -46,6 +47,8 @@ class CronManager implements CronManagerInterface private const UPDATE_BEFORE_EXEC = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastStart = :now WHERE cr.key = :key'; + private const UPDATE_LAST_EXECUTION_DATA = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastExecutionData = :data WHERE cr.key = :key'; + private CronJobExecutionRepositoryInterface $cronJobExecutionRepository; private EntityManagerInterface $entityManager; @@ -85,6 +88,9 @@ class CronManager implements CronManagerInterface foreach ($orderedJobs as $job) { if ($job->canRun($lasts[$job->getKey()] ?? null)) { if (array_key_exists($job->getKey(), $lasts)) { + + $executionData = $lasts[$job->getKey()]->getLastExecutionData(); + $this->entityManager ->createQuery(self::UPDATE_BEFORE_EXEC) ->setParameters([ @@ -96,12 +102,17 @@ class CronManager implements CronManagerInterface $execution = new CronJobExecution($job->getKey()); $this->entityManager->persist($execution); $this->entityManager->flush(); + + $executionData = $execution->getLastExecutionData(); } $this->entityManager->clear(); + // note: at this step, the entity manager does not have any entity CronJobExecution + // into his internal memory + try { $this->logger->info(sprintf('%sWill run job', self::LOG_PREFIX), ['job' => $job->getKey()]); - $job->run(); + $result = $job->run($executionData); $this->entityManager ->createQuery(self::UPDATE_AFTER_EXEC) @@ -112,6 +123,14 @@ class CronManager implements CronManagerInterface ]) ->execute(); + if (null !== $result) { + $this->entityManager + ->createQuery(self::UPDATE_LAST_EXECUTION_DATA) + ->setParameter('data', $result, Types::JSON) + ->setParameter('key', $job->getKey(), Types::STRING) + ->execute(); + } + $this->logger->info(sprintf('%sSuccessfully run job', self::LOG_PREFIX), ['job' => $job->getKey()]); return; @@ -133,7 +152,7 @@ class CronManager implements CronManagerInterface } /** - * @return array<0: CronJobInterface[], 1: array> + * @return array{0: array, 1: array} */ private function getOrderedJobs(): array { @@ -174,7 +193,7 @@ class CronManager implements CronManagerInterface { foreach ($this->jobs as $job) { if ($job->getKey() === $forceJob) { - $job->run(); + $job->run([]); } } } diff --git a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php index 9dbb38a3f..3a6ff8fb9 100644 --- a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php @@ -49,8 +49,10 @@ class RefreshAddressToGeographicalUnitMaterializedViewCronJob implements CronJob return 'refresh-materialized-view-address-to-geog-units'; } - public function run(): void + public function run(array $lastExecutionData): null|array { $this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); + + return null; } } diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php new file mode 100644 index 000000000..0b80730fe --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php @@ -0,0 +1,87 @@ +entityManager = self::$container->get(EntityManagerInterface::class); + $this->cronJobExecutionRepository = self::$container->get(CronJobExecutionRepository::class); + } + + public function testCompleteLifeCycle(): void + { + $cronjob = $this->prophesize(CronJobInterface::class); + $cronjob->canRun(null)->willReturn(true); + $cronjob->canRun(Argument::type(CronJobExecution::class))->willReturn(true); + $cronjob->getKey()->willReturn('test-with-data'); + $cronjob->run([])->willReturn(['test' => 'execution-0']); + $cronjob->run(['test' => 'execution-0'])->willReturn(['test' => 'execution-1']); + + $cronjob->run([])->shouldBeCalledOnce(); + $cronjob->run(['test' => 'execution-0'])->shouldBeCalledOnce(); + + $manager = new CronManager( + $this->cronJobExecutionRepository, + $this->entityManager, + [$cronjob->reveal()], + new NullLogger() + ); + + // run a first time + $manager->run(); + + // run a second time + $manager->run(); + } + +} + +class JobWithReturn implements CronJobInterface +{ + public function canRun(?CronJobExecution $cronJobExecution): bool + { + return true; + } + + public function getKey(): string + { + return 'with-data'; + } + + public function run(array $lastExecutionData): null|array + { + return ['data' => 'test']; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php index 4b812ce2b..47c929a52 100644 --- a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php @@ -40,7 +40,7 @@ final class CronManagerTest extends TestCase $jobToExecute = $this->prophesize(CronJobInterface::class); $jobToExecute->getKey()->willReturn('to-exec'); $jobToExecute->canRun(Argument::type(CronJobExecution::class))->willReturn(true); - $jobToExecute->run()->shouldBeCalled(); + $jobToExecute->run([])->shouldBeCalled(); $executions = [ ['key' => $jobOld1->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS], @@ -64,7 +64,7 @@ final class CronManagerTest extends TestCase $jobAlreadyExecuted = new JobCanRun('k'); $jobNeverExecuted = $this->prophesize(CronJobInterface::class); $jobNeverExecuted->getKey()->willReturn('never-executed'); - $jobNeverExecuted->run()->shouldBeCalled(); + $jobNeverExecuted->run([])->shouldBeCalled(); $jobNeverExecuted->canRun(null)->willReturn(true); $executions = [ @@ -86,7 +86,7 @@ final class CronManagerTest extends TestCase $jobAlreadyExecuted = new JobCanRun('k'); $jobNeverExecuted = $this->prophesize(CronJobInterface::class); $jobNeverExecuted->getKey()->willReturn('never-executed'); - $jobNeverExecuted->run()->shouldBeCalled(); + $jobNeverExecuted->run([])->shouldBeCalled(); $jobNeverExecuted->canRun(null)->willReturn(true); $executions = [ @@ -178,8 +178,9 @@ class JobCanRun implements CronJobInterface return $this->key; } - public function run(): void + public function run(array $lastExecutionData): null|array { + return null; } } @@ -195,7 +196,8 @@ class JobCannotRun implements CronJobInterface return 'job-b'; } - public function run(): void + public function run(array $lastExecutionData): null|array { + return null; } } diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php index f637e70b9..2ddf3415c 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php @@ -39,8 +39,10 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface return 'accompanying-period-step-change'; } - public function run(): void + public function run(array $lastExecutionData): null|array { ($this->requestor)(); + + return null; } } From e82c7cdc6c174c6873775a43d20446668af3eb43 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 13:24:23 +0200 Subject: [PATCH 44/87] Fixed: [homepage widget] repair my unread notification list with actions and evaluations documents --- .../public/vuejs/HomepageWidget/MyNotifications.vue | 8 ++++++++ .../Resources/public/vuejs/HomepageWidget/js/i18n.js | 1 + 2 files changed, 9 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue index 06683f8fc..bfde741ac 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue @@ -66,6 +66,10 @@ export default { return appMessages.fr.the_activity; case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod': return appMessages.fr.the_course; + case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork': + return appMessages.fr.the_action; + case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument': + return appMessages.fr.the_evaluation_document; case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow': return appMessages.fr.the_workflow; default: @@ -78,6 +82,10 @@ export default { return `/fr/activity/${n.relatedEntityId}/show` case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod': return `/fr/parcours/${n.relatedEntityId}` + case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork': + return `/fr/person/accompanying-period/work/${n.relatedEntityId}/show` + case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument': + return `/fr/notification/${n.id}/show` // to the notification case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow': return `/fr/main/workflow/${n.relatedEntityId}/show` default: diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js index 697074671..587f06fa9 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js @@ -46,6 +46,7 @@ const appMessages = { the_course: "le parcours", the_action: "l'action", the_evaluation: "l'évaluation", + the_evaluation_document: "le document de l'évaluation", the_task: "la tâche", the_workflow: "le workflow", StartDate: "Date d'ouverture", From e0758215ba3c557fc26065591663be2389e1fe50 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 12 Jul 2023 15:17:03 +0200 Subject: [PATCH 45/87] FEATURE [repository] implement filter logic --- src/Bundle/ChillMainBundle/Entity/Address.php | 1 + .../Templating/Listing/FilterOrderHelper.php | 3 +- .../AccompanyingCourseWorkController.php | 4 +- .../AccompanyingPeriodWorkRepository.php | 42 ++++++++++++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 1bd1a453a..9fcb07fe5 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -255,6 +255,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface public function syncWithReference(AddressReference $addressReference): Address { + dump($addressReference); $this ->setPoint($addressReference->getPoint()) ->setPostcode($addressReference->getPostcode()) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 8554b4431..6a4d07167 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -134,7 +135,7 @@ class FilterOrderHelper return $this->userPickers; } - public function getUserPickerData(string $name): array + public function getUserPickerData(string $name) { return $this->getFormData()['user_pickers'][$name]; } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index c56489afd..3eb6e67f5 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -161,7 +161,7 @@ class AccompanyingCourseWorkController extends AbstractController 'types' => $filter->getEntityChoiceData('typesFilter'), 'before' => $filter->getDateRangeData('dateFilter')['to'], 'after' => $filter->getDateRangeData('dateFilter')['from'], - 'users' => $filter->getUserPickerData('userFilter') + 'user' => $filter->getUserPickerData('userFilter') ]; $totalItems = $this->workRepository->countByAccompanyingPeriod($period); @@ -226,7 +226,7 @@ class AccompanyingCourseWorkController extends AbstractController ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialWork\SocialAction::class, $types, [ 'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle()) ]) - ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false]) + ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false, 'multiple' => false]) ; return $filterBuilder->build(); diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index a5e27afd5..6fc8ff86e 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -103,40 +103,52 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w - WHERE accompanyingPeriod_id = :periodId - ORDER BY - CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, - startdate DESC, - enddate DESC, - id DESC"; + WHERE accompanyingPeriod_id = :periodId"; // implement filters if([] !== ($filters['types'] ?? [])) { - $sql .= "AND WHERE w.socialAction IN (:types)"; + $sql .= " AND w.socialaction_id IN (:types)"; } if([] !== ($filters['users'] ?? [])) { - $sql .= "AND WHERE w.createdBy IN (:users)"; + $sql .= " AND w.createdBy = (:userCreated)"; - foreach ($filters['users'] as $key => $user) { - $sql .= "OR :user_" . $key . " IN w.referrers)"; + $sql .= " OR :userReferrer IN (w.referrers)"; + } - $nq = $this->em->createNativeQuery($sql, $rsm) - ->setParameter(':user_' . $key); - } - - // ... to be continued + if (null !== ($after = $filters['after'] ?? null) && null === $filters['before']) { + $sql .= " AND w.startdate::date >= :after"; + } elseif (null !== ($before = $filters['before'] ?? null) && null === $filters['after']) { + $sql .= " AND COALESCE(w.enddate::date, 'infinity'::date) <= :before"; + } elseif (null !== ($after = $filters['after'] ?? null) && null !== ($before = $filters['before'] ?? null)) { + $sql .= " AND w.startdate::date >= :after AND COALESCE(w.enddate::date, 'now'::date) <= :before"; } // set limit and offset + $sql .= " ORDER BY + CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, + startdate DESC, + enddate DESC, + id DESC"; + $sql .= " LIMIT :limit OFFSET :offset"; + $typeIds = []; + foreach ($filters['types'] as $type) { + $typeIds[] = $type->getId(); + } + $nq = $this->em->createNativeQuery($sql, $rsm) ->setParameter('periodId', $period->getId(), Types::INTEGER) + ->setParameter('types', $typeIds) + ->setParameter('userCreated', $filters['user']) + ->setParameter('userReferrer', $filters['user']) + ->setParameter('after', $filters['after']) + ->setParameter('before', $filters['before']) ->setParameter('limit', $limit, Types::INTEGER) ->setParameter('offset', $offset, Types::INTEGER); From 29306d2b66bcd6a1e3882391ed22c863140bbbc6 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 24 May 2023 19:55:17 +0200 Subject: [PATCH 46/87] UX: [vue][onTheFly] improve residential address position in modale --- .../_components/Entity/AddressRenderBox.vue | 2 +- .../_components/Entity/PersonRenderBox.vue | 93 +++++++------------ 2 files changed, 33 insertions(+), 62 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue index 14a376856..00fb2b996 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue @@ -1,6 +1,6 @@