Merge remote-tracking branch 'origin/master' into rector/rules-symfony

This commit is contained in:
Julien Fastré 2023-10-17 09:29:10 +02:00
commit adf1110340
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
17 changed files with 203 additions and 148 deletions

View File

@ -1,5 +0,0 @@
kind: Fixed
body: 'View a third party: avoid errors when a contact has a civility'
time: 2023-09-28T12:09:10.199359071+02:00
custom:
Issue: "164"

19
.changes/v2.8.0.md Normal file
View File

@ -0,0 +1,19 @@
## v2.8.0 - 2023-10-05
### Feature
* ([#162](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/162)) Reassigning list: when reassigning courses to a new user, the job associated with the course become the one of the new user (if any)
* Reassining list: the length of the list is increased to 100 courses
### Fixed
* ([#143](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/143)) Fix filter "accompanying course by social action" to avoid duplication in list
* ([#164](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/164)) View a third party: avoid errors when a contact has a civility
* ([#163](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/163)) Fix the filters and aggregators on exports "count peoples"
* ([#143](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/143)) From the database, avoid the creation of location history for same period and at same dates
### Traduction francophone des principaux changements
- Fonctionnalité: Réassigner les parcours en lot: lorsque des parcours sont réassignés "en lot", les parcours sont maintenant associés au métier du nouveau référent;
- Correction: certaines causes qui créaient des doublons dans les listes ont été corrigées;
- Correction des associations entre l'export "nombre de personnes" et les filtres et regroupements associés

View File

@ -6,6 +6,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.8.0 - 2023-10-05
### Feature
* ([#162](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/162)) Reassigning list: when reassigning courses to a new user, the job associated with the course become the one of the new user (if any)
* Reassining list: the length of the list is increased to 100 courses
### Fixed
* ([#143](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/143)) Fix filter "accompanying course by social action" to avoid duplication in list
* ([#164](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/164)) View a third party: avoid errors when a contact has a civility
* ([#163](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/163)) Fix the filters and aggregators on exports "count peoples"
* ([#143](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/143)) From the database, avoid the creation of location history for same period and at same dates
### Traduction francophone des principaux changements
- Fonctionnalité: Réassigner les parcours en lot: lorsque des parcours sont réassignés "en lot", les parcours sont maintenant associés au métier du nouveau référent;
- Correction: certaines causes qui créaient des doublons dans les listes ont été corrigées;
- Correction des associations entre l'export "nombre de personnes" et les filtres et regroupements associés
## v2.7.0 - 2023-09-27
### Feature
* ([#155](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/155)) The regulation list load accompanying periods by exact postal code (address associated with postal code), and not by the content of the postal code (postal code with same code's string)

View File

@ -40,7 +40,7 @@ About once a year, the core team discusses the opportunity to invite new members
### Core Membership Revocation
A Symfony Core membership can be revoked for any of the following reasons:
A Chill Core membership can be revoked for any of the following reasons:
- Refusal to follow the rules and policies stated in this document;
- Lack of activity for the past six months;

View File

@ -16,31 +16,22 @@ use Chill\ActivityBundle\Entity\ActivityReason;
use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\Export\FilterType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInterface, FilterInterface
final readonly class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelper $translatableStringHelper,
protected ActivityReasonRepository $activityReasonRepository,
protected TranslatorInterface $translator
private TranslatableStringHelper $translatableStringHelper,
private ActivityReasonRepository $activityReasonRepository,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
@ -52,16 +43,19 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
{
// create a subquery for activity
$sqb = $qb->getEntityManager()->createQueryBuilder();
$sqb->select('person_person_having_activity.id')
$sqb->select('1')
->from(Activity::class, 'activity_person_having_activity')
->join('activity_person_having_activity.person', 'person_person_having_activity');
->leftJoin('activity_person_having_activity.person', 'person_person_having_activity');
// add clause between date
$sqb->where('activity_person_having_activity.date BETWEEN '
. ':person_having_activity_between_date_from'
. ' AND '
. ':person_having_activity_between_date_to');
. ':person_having_activity_between_date_to'
. ' AND '
. '(person_person_having_activity.id = person.id OR person MEMBER OF activity_person_having_activity.persons)');
if (isset($data['reasons']) && [] !== $data['reasons']) {
// add clause activity reason
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
@ -72,111 +66,70 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
)
);
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->in('person.id', $sqb->getDQL());
if ($where instanceof Expr\Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
$qb->setParameter('person_having_activity_reasons', $data['reasons']);
}
$qb->add('where', $where);
$qb->andWhere(
$qb->expr()->exists($sqb->getDQL())
);
$qb->setParameter(
'person_having_activity_between_date_from',
$data['date_from']
$this->rollingDateConverter->convert($data['date_from_rolling'])
);
$qb->setParameter(
'person_having_activity_between_date_to',
$data['date_to']
$this->rollingDateConverter->convert($data['date_to_rolling'])
);
$qb->setParameter('person_having_activity_reasons', $data['reasons']);
}
public function applyOn(): string
{
return Declarations::PERSON_IMPLIED_IN;
return Declarations::PERSON_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('date_from', DateType::class, [
'label' => 'Implied in an activity after this date',
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
$builder->add('date_from_rolling', PickRollingDateType::class, [
'label' => 'export.filter.activity.person_between_dates.Implied in an activity after this date',
]);
$builder->add('date_to', DateType::class, [
'label' => 'Implied in an activity before this date',
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
$builder->add('date_to_rolling', PickRollingDateType::class, [
'label' => 'export.filter.activity.person_between_dates.Implied in an activity before this date',
]);
if ([] !== $reasons = $this->activityReasonRepository->findAll()) {
$builder->add('reasons', EntityType::class, [
'class' => ActivityReason::class,
'choices' => $reasons,
'choice_label' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getName()),
'group_by' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getCategory()->getName()),
'multiple' => true,
'expanded' => false,
'label' => 'Activity reasons for those activities',
'label' => 'export.filter.activity.person_between_dates.Activity reasons for those activities',
'help' => 'export.filter.activity.person_between_dates.if no reasons',
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
/** @var FormInterface $filterForm */
$filterForm = $event->getForm()->getParent();
$enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData();
if (true === $enabled) {
// if the filter is enabled, add some validation
$form = $event->getForm();
$date_from = $form->get('date_from')->getData();
$date_to = $form->get('date_to')->getData();
// check that fields are not empty
if (null === $date_from) {
$form->get('date_from')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
if (null === $date_to) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
// check that date_from is before date_to
if (
(null !== $date_from && null !== $date_to)
&& $date_from >= $date_to
) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('This date '
. 'should be after the date given in "Implied in an '
. 'activity after this date" field')
));
}
}
});
}
public function getFormDefaultData(): array
{
return ['date_from' => new DateTime(), 'date_to' => new DateTime(), 'reasons' => $this->activityReasonRepository->findAll()];
return [
'date_from_rolling' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
'date_to_rolling' => new RollingDate(RollingDate::T_TODAY),
'reasons' => [],
];
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by person having an activity between %date_from% and '
. '%date_to% with reasons %reasons_name%',
[] === $data['reasons'] ?
'export.filter.person_between_dates.describe_action_with_no_subject'
: 'export.filter.person_between_dates.describe_action_with_subject',
[
'%date_from%' => $data['date_from']->format('d-m-Y'),
'%date_to%' => $data['date_to']->format('d-m-Y'),
'%reasons_name%' => implode(
'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
'reasons' => implode(
', ',
array_map(
fn (ActivityReason $r): string => '"' . $this->translatableStringHelper->localize($r->getName()) . '"',
@ -189,13 +142,15 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
public function getTitle()
{
return 'Filter by person having an activity in a period';
return 'export.filter.activity.person_between_dates.title';
}
public function validateForm($data, ExecutionContextInterface $context)
{
if (null === $data['reasons'] || count($data['reasons']) === 0) {
$context->buildViolation('At least one reason must be chosen')
if ($this->rollingDateConverter->convert($data['date_from_rolling'])
>= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
$context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
->setTranslationDomain('messages')
->addViolation();
}
}

View File

@ -3,3 +3,9 @@ export:
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}
person_between_dates:
describe_action_with_no_subject: >-
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}
describe_action_with_subject: >-
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons}

View File

@ -255,11 +255,6 @@ Activities before this date: Échanges avant cette date
"Filtered by date of activity: only between %date_from% and %date_to%": "Filtré par date de l'échange: uniquement entre %date_from% et %date_to%"
This date should be after the date given in "Implied in an activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "échanges après cette date"
Filtered by person having an activity in a period: Uniquement les usagers ayant eu un échange dans la période donnée
Implied in an activity after this date: Impliqué dans un échange après cette date
Implied in an activity before this date: Impliqué dans un échange avant cette date
Filtered by person having an activity between %date_from% and %date_to% with reasons %reasons_name%: Filtré par usager associées à un échange entre %date_from% et %date_to% avec les sujets %reasons_name%
Activity reasons for those activities: Sujets de ces échanges
Filter by activity type: Filtrer les échanges par type
@ -372,6 +367,13 @@ export:
Receiving an activity before: Ayant reçu un échange avant le
acp_by_activity_type:
'acp_containing_at_least_one_%activitytypes%': 'Parcours filtrés: uniquement ceux qui contiennent au moins un échange d''un des types suivants: %activitytypes%'
person_between_dates:
Implied in an activity after this date: Impliqué dans un échange après cette date
Implied in an activity before this date: Impliqué dans un échange avant cette date
Activity reasons for those activities: Sujets de ces échanges
if no reasons: Si aucun sujet n'est coché, tous les sujets seront pris en compte
title: Filtrer les personnes ayant été associés à un échange au cours de la période
date mismatch: La date de fin de la période doit être supérieure à la date du début
by_creator_scope:
Filter activity by user scope: Filtrer les échanges par service du créateur de l'échange
'Filtered activity by user scope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"

View File

@ -33,9 +33,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Validator\Constraints\NotIdenticalTo;
use Symfony\Component\Validator\Constraints\NotNull;
use function is_int;
@ -62,7 +60,7 @@ class ReassignAccompanyingPeriodController extends AbstractController
$total = $this->accompanyingPeriodACLAwareRepository->countByUserAndPostalCodesOpenedAccompanyingPeriod($userFrom, $postalCodes);
$paginator = $this->paginatorFactory->create($total);
$paginator->setItemsPerPage(50);
$paginator->setItemsPerPage(100);
$periods = $this->accompanyingPeriodACLAwareRepository
->findByUserAndPostalCodesOpenedAccompanyingPeriod(
$userFrom,
@ -85,6 +83,7 @@ class ReassignAccompanyingPeriodController extends AbstractController
if ($assignForm->isSubmitted() && $assignForm->isValid()) {
$assignPeriodIds = json_decode((string) $assignForm->get('periods')->getData(), true, 512, JSON_THROW_ON_ERROR);
/** @var User $userTo */
$userTo = $assignForm->get('userTo')->getData();
$userFrom = $assignForm->get('userFrom')->getData();
@ -93,6 +92,10 @@ class ReassignAccompanyingPeriodController extends AbstractController
if ($period->getUser() === $userFrom) {
$period->setUser($userTo, true);
if (null !== $userTo->getUserJob() && $period->getJob() !== $userTo->getUserJob()) {
$period->setJob($userTo->getUserJob());
}
}
}

View File

@ -22,7 +22,10 @@ abstract class Declarations
final public const HOUSEHOLD_TYPE = 'household';
final public const PERSON_IMPLIED_IN = 'person_implied_in';
/**
* @deprecated consider using the PERSON_TYPE instead
*/
public const PERSON_IMPLIED_IN = 'person_implied_in';
final public const PERSON_TYPE = 'person';

View File

@ -114,9 +114,7 @@ class CountPerson implements ExportInterface, GroupedExportInterface
public function supportsModifiers()
{
return [
'abcde',
//Declarations::PERSON_TYPE,
//Declarations::PERSON_IMPLIED_IN,
Declarations::PERSON_TYPE,
];
}
}

View File

@ -250,7 +250,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
public function supportsModifiers()
{
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN];
return [Declarations::PERSON_TYPE];
}
public function validateForm($data, ExecutionContextInterface $context)

View File

@ -194,7 +194,7 @@ class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterf
public function supportsModifiers()
{
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE];
return [Declarations::PERSON_TYPE, Declarations::ACP_TYPE];
}
public function validateForm($data, ExecutionContextInterface $context)

View File

@ -152,6 +152,6 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte
public function supportsModifiers()
{
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE];
return [Declarations::PERSON_TYPE, Declarations::ACP_TYPE];
}
}

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Form\Type\PickSocialActionType;
@ -21,9 +21,9 @@ use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class SocialActionFilter implements FilterInterface
final readonly class SocialActionFilter implements FilterInterface
{
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly SocialActionRender $actionRender) {}
public function __construct(private SocialActionRender $actionRender) {}
public function addRole(): ?string
{
@ -32,21 +32,17 @@ class SocialActionFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acpw', $qb->getAllAliases(), true)) {
$qb->join('acp.works', 'acpw');
}
if (!in_array('acpwsocialaction', $qb->getAllAliases(), true)) {
$qb->join('acpw.socialAction', 'acpwsocialaction');
}
$clause = $qb->expr()->in('acpwsocialaction.id', ':socialactions');
$qb->andWhere($clause)
->setParameter(
'socialactions',
SocialAction::getDescendantsWithThisForActions($data['accepted_socialactions'])->toArray()
$qb->andWhere(
$qb->expr()->exists(
sprintf(
"SELECT 1 FROM %s acp_by_social_action_filter WHERE acp_by_social_action_filter.socialAction "
. "IN (:acp_by_social_action_filter_actions) AND acp_by_social_action_filter.accompanyingPeriod = acp",
AccompanyingPeriod\AccompanyingPeriodWork::class
)
)
);
$qb->setParameter('acp_by_social_action_filter_actions', SocialAction::getDescendantsWithThisForActions($data['accepted_socialactions']));
}
public function applyOn(): string

View File

@ -26,9 +26,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class ClosingMotiveType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator)
{
}
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20231002094521 extends AbstractMigration
{
public function getDescription(): string
{
return 'Remove doublons in chill_person_accompanying_period_location_history and add a constraint on startdate / endDate by period';
}
public function up(Schema $schema): void
{
$this->addSql(
<<<'SQL'
WITH doublons_ordered AS (
SELECT h2.id AS h2id, h2.createdAt AS h2createdAt, h2.startDate AS h2start, h2.endDate AS h2end, h1.*,
rank() OVER (partition by h1.period_id ORDER BY h1.id, h2.id) AS ranking
FROM chill_person_accompanying_period_location_history h1
JOIN chill_person_accompanying_period_location_history h2 ON h1.period_id = h2.period_id AND h1.id <> h2.id
WHERE daterange(h1.startdate, h1.enddate) && daterange(h2.startdate, h2.enddate) ORDER BY h1.period_id, h1.id
),
keep_only_first AS (
SELECT id FROM doublons_ordered WHERE ranking > 1
)
DELETE FROM chill_person_accompanying_period_location_history WHERE id IN (SELECT id FROM doublons_ordered);
SQL
);
$this->addSql(
<<<'SQL'
ALTER TABLE chill_person_accompanying_period_location_history
ADD CONSTRAINT acc_period_location_history_not_overlaps
EXCLUDE USING GIST (period_id with =, tsrange(startdate, enddate) with &&)
DEFERRABLE INITIALLY DEFERRED
SQL
);
}
public function down(Schema $schema): void
{
$this->addSql(
<<<'SQL'
ALTER TABLE chill_person_accompanying_period_location_history DROP CONSTRAINT acc_period_location_history_not_overlaps
SQL
);
}
}

View File

@ -426,7 +426,7 @@ class ReportList implements ExportElementValidatedInterface, ListInterface
public function supportsModifiers()
{
return [Declarations::PERSON_IMPLIED_IN, Declarations::PERSON_TYPE, 'report'];
return [Declarations::PERSON_TYPE, 'report'];
}
public function validateForm($data, ExecutionContextInterface $context)