Merge branch '185-fix-inconsistencies-person-geog-unit-aggregator' into 'master'

Fix geographical unit stat aggregator by course: show also the course if they...

Closes #194 and #185

See merge request Chill-Projet/chill-bundles!607
This commit is contained in:
Julien Fastré 2023-11-07 15:15:12 +00:00
commit f756c0d71e
12 changed files with 255 additions and 20 deletions

View File

@ -0,0 +1,5 @@
kind: Feature
body: 'Export: add a filter "filter activity by creator job"'
time: 2023-11-07T16:04:35.921093231+01:00
custom:
Issue: "194"

View File

@ -0,0 +1,6 @@
kind: Fixed
body: 'Export: fix "group accompanying period by geographical unit": take into account
the accompanying periods when the period is not located within an unit'
time: 2023-11-07T14:40:39.46483404+01:00
custom:
Issue: "185"

View File

@ -0,0 +1,5 @@
kind: Fixed
body: Fix "group activity by creator job" aggregator
time: 2023-11-07T16:07:02.576354444+01:00
custom:
Issue: ""

View File

@ -14,18 +14,18 @@ namespace Chill\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class JobScopeAggregator implements AggregatorInterface
class CreatorJobAggregator implements AggregatorInterface
{
private const PREFIX = 'acp_agg_creator_job';
public function __construct(
private readonly ScopeRepository $scopeRepository,
private readonly UserJobRepositoryInterface $userJobRepository,
private readonly TranslatableStringHelper $translatableStringHelper
) {}
@ -76,17 +76,15 @@ class JobScopeAggregator implements AggregatorInterface
{
return function ($value): string {
if ('_header' === $value) {
return 'Scope';
return 'Job';
}
if (null === $value || '' === $value) {
if (null === $value || '' === $value || null === $s = $this->userJobRepository->find($value)) {
return '';
}
$s = $this->scopeRepository->find($value);
return $this->translatableStringHelper->localize(
$s->getName()
$s->getLabel()
);
};
}

View File

@ -0,0 +1,116 @@
<?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\ActivityBundle\Export\Filter;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class CreatorJobFilter implements FilterInterface
{
private const PREFIX = 'acp_act_filter_creator_job';
public function __construct(
private TranslatableStringHelper $translatableStringHelper,
private TranslatorInterface $translator,
private UserJobRepositoryInterface $userJobRepository,
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$p = self::PREFIX;
$qb
->leftJoin('activity.createdBy', "{$p}_user")
->leftJoin(
UserJobHistory::class,
"{$p}_history",
Join::WITH,
$qb->expr()->eq("{$p}_history.user", "{$p}_user")
)
// job_at based on activity.date
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte("{$p}_history.startDate", 'activity.date'),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_history.endDate"),
$qb->expr()->gt("{$p}_history.endDate", 'activity.date')
)
)
)
->andWhere(
$qb->expr()->in("{$p}_history.job", ":{$p}_jobs")
)
->setParameter(
"{$p}_jobs",
$data['jobs'],
);
}
public function applyOn(): string
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('jobs', EntityType::class, [
'choices' => $this->userJobRepository->findAllOrderedByName(),
'class' => UserJob::class,
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
$s->getLabel()
).($s->isActive() ? '' : '('.$this->translator->trans('inactive').')'),
'label' => 'export.filter.activity.by_creator_job.job_form_label',
'multiple' => true,
'expanded' => true,
]);
}
public function describeAction($data, $format = 'string'): array
{
$jobs = array_map(
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
);
return ['export.filter.activity.by_creator_job.Filtered activity by user job: only %jobs%', [
'%jobs%' => implode(', ', $jobs),
]];
}
public function getFormDefaultData(): array
{
return [
'jobs' => [],
];
}
public function getTitle(): string
{
return 'export.filter.activity.by_creator_job.Filter activity by user job';
}
}

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Export\Aggregator;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Aggregator\JobScopeAggregator;
use Chill\ActivityBundle\Export\Aggregator\CreatorJobAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
@ -21,15 +21,15 @@ use Doctrine\ORM\EntityManagerInterface;
*
* @coversNothing
*/
final class JobScopeAggregatorTest extends AbstractAggregatorTest
final class CreatorJobAggregatorTest extends AbstractAggregatorTest
{
private JobScopeAggregator $aggregator;
private CreatorJobAggregator $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::$container->get(JobScopeAggregator::class);
$this->aggregator = self::$container->get(CreatorJobAggregator::class);
}
public function getAggregator()

View File

@ -0,0 +1,75 @@
<?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\ActivityBundle\Tests\Export\Filter;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Filter\CreatorJobFilter;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class CreatorJobFilterTest extends AbstractFilterTest
{
private EntityManagerInterface $entityManager;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
private UserJobRepositoryInterface $userJobRepository;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
$this->translatableStringHelper = self::$container->get(TranslatableStringHelperInterface::class);
$this->translator = self::$container->get(TranslatorInterface::class);
$this->userJobRepository = self::$container->get(UserJobRepositoryInterface::class);
}
public function getFilter()
{
return new CreatorJobFilter(
$this->translatableStringHelper,
$this->translator,
$this->userJobRepository
);
}
public function getFormData()
{
$this->setUp();
$jobs = $this->userJobRepository->findAll();
return [
['jobs' => $jobs],
];
}
public function getQueryBuilders()
{
self::setUp();
return [
$this->entityManager->createQueryBuilder()
->select('count(activity.id)')
->from(Activity::class, 'activity')
->join('activity.user', 'actuser'),
];
}
}

View File

@ -115,6 +115,10 @@ services:
# affect all saved exports (unless we write a migration for that)
- { name: chill.export_filter, alias: 'activity_userscope_filter' }
Chill\ActivityBundle\Export\Filter\CreatorJobFilter:
tags:
- { name: chill.export_filter, alias: 'activity_creatorjob_filter' }
Chill\ActivityBundle\Export\Filter\UsersJobFilter:
tags:
- { name: chill.export_filter, alias: 'activity_usersjob_filter' }
@ -179,7 +183,7 @@ services:
tags:
- { name: chill.export_aggregator, alias: activity_creator_scope_aggregator }
Chill\ActivityBundle\Export\Aggregator\JobScopeAggregator:
Chill\ActivityBundle\Export\Aggregator\CreatorJobAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_creator_job_aggregator }

View File

@ -376,7 +376,11 @@ export:
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%"
'Filtered activity by user scope: only %scopes%': "Filtré par service du créateur de l'échange: uniquement %scopes%"
by_creator_job:
job_form_label: Métiers
Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange
'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%"
by_persons:
Filter activity by persons: Filtrer les échanges par usager participant
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'
@ -412,8 +416,8 @@ export:
Group activity by creator scope: Grouper les échanges par service du créateur de l'échange
Calc date: Date de calcul du service du créateur de l'échange
by_creator_job:
Group activity by creator job: Grouper les échanges par service du créateur de l'échange
Calc date: Date de calcul du service du créateur de l'échange
Group activity by creator job: Grouper les échanges par métier du créateur de l'échange
Calc date: Date de calcul du métier du créateur de l'échange
by_persons:
Group activity by persons: Grouper les échanges par usager participant
Persons: Usagers participants

View File

@ -12,14 +12,15 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
class UserJobRepository implements UserJobRepositoryInterface
readonly class UserJobRepository implements UserJobRepositoryInterface
{
private readonly EntityRepository $repository;
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, private TranslatableStringHelperInterface $translatableStringHelper)
{
$this->repository = $em->getRepository(UserJob::class);
}
@ -42,6 +43,15 @@ class UserJobRepository implements UserJobRepositoryInterface
return $this->repository->findBy(['active' => true]);
}
public function findAllOrderedByName(): array
{
$jobs = $this->findAll();
usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel()));
return $jobs;
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@ -28,6 +28,13 @@ interface UserJobRepositoryInterface extends ObjectRepository
*/
public function findAllActive(): array;
/**
* a list of UserJob ordered by name.
*
* @return array<UserJob>
*/
public function findAllOrderedByName(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@ -83,7 +83,12 @@ final readonly class GeographicalUnitStatAggregator implements AggregatorInterfa
'acp_geog_units'
);
$qb->andWhere($qb->expr()->in('acp_geog_units.layer', ':acp_geog_unit_layer'));
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('acp_geog_units'),
$qb->expr()->in('acp_geog_units.layer', ':acp_geog_unit_layer')
)
);
$qb->setParameter('acp_geog_unit_layer', $data['level']);