Merge remote-tracking branch 'origin/master' into testing

This commit is contained in:
2023-06-27 21:11:09 +02:00
23 changed files with 355 additions and 66 deletions

View File

@@ -12,14 +12,16 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Symfony\Component\Security\Core\Security;
final readonly class PersonDocumentGenericDocProvider implements GenericDocForPersonProviderInterface
final readonly class PersonDocumentGenericDocProvider implements GenericDocForPersonProviderInterface, GenericDocForAccompanyingPeriodProviderInterface
{
public const KEY = 'person_document';
@@ -48,4 +50,16 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe
{
return $this->security->isGranted(PersonDocumentVoter::SEE, $person);
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
return $this->personDocumentACLAwareRepository->buildFetchQueryForAccompanyingPeriod($accompanyingPeriod, $startDate, $endDate, $content);
}
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
{
// we assume that the user is allowed to see at least one person of the course
// this will be double checked when running the query
return true;
}
}

View File

@@ -22,6 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
@@ -29,19 +31,14 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface
final readonly class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface
{
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
private CenterResolverManagerInterface $centerResolverManager;
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $em, CenterResolverManagerInterface $centerResolverManager, AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser)
{
$this->em = $em;
$this->centerResolverManager = $centerResolverManager;
$this->authorizationHelperForCurrentUser = $authorizationHelperForCurrentUser;
public function __construct(
private EntityManagerInterface $em,
private CenterResolverManagerInterface $centerResolverManager,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
private Security $security,
) {
}
public function buildQueryByPerson(Person $person): QueryBuilder
@@ -63,6 +60,66 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
return $this->addFetchQueryByPersonACL($query, $person);
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
$participationMetadata = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class);
$query = new FetchQuery(
PersonDocumentGenericDocProvider::KEY,
sprintf('jsonb_build_object(\'id\', person_document.%s)', $personDocMetadata->getSingleIdentifierColumnName()),
sprintf('person_document.%s', $personDocMetadata->getColumnName('date')),
sprintf('%s AS person_document', $personDocMetadata->getSchemaName().'.'.$personDocMetadata->getTableName())
);
$query->addJoinClause(
sprintf(
'JOIN %s AS participation ON participation.%s = person_document.%s '.
'AND person_document.%s BETWEEN participation.%s AND COALESCE(participation.%s, \'infinity\'::date)',
$participationMetadata->getTableName(),
$participationMetadata->getSingleAssociationJoinColumnName('person'),
$personDocMetadata->getSingleAssociationJoinColumnName('person'),
$personDocMetadata->getColumnName('date'),
$participationMetadata->getColumnName('startDate'),
$participationMetadata->getColumnName('endDate')
)
);
$query->addWhereClause(
sprintf('participation.%s = ?', $participationMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod')),
[$period->getId()],
[Types::INTEGER]
);
// can we see the document for this person ?
$orPersonId = [];
foreach ($period->getParticipations() as $participation) {
if (!$this->security->isGranted(PersonDocumentVoter::SEE, $participation->getPerson())) {
continue;
}
$orPersonId[] = $participation->getPerson()->getId();
}
if ([] === $orPersonId) {
$query->addWhereClause('FALSE = TRUE');
return $query;
}
$query->addWhereClause(
sprintf(
'participation.%s IN (%s)',
$participationMetadata->getSingleAssociationJoinColumnName('person'),
implode(', ', array_fill(0, count($orPersonId), '?'))
),
$orPersonId,
array_fill(0, count($orPersonId), Types::INTEGER)
);
return $this->addFilterClauses($query, $startDate, $endDate, $content);
}
public function buildBaseFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
@@ -80,6 +137,13 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
[Types::INTEGER]
);
return $this->addFilterClauses($query, $startDate, $endDate, $content);
}
private function addFilterClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
if (null !== $startDate) {
$query->addWhereClause(
sprintf('? <= %s', $personDocMetadata->getColumnName('date')),
@@ -107,7 +171,6 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
[Types::STRING, Types::STRING]
);
}
return $query;
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
interface PersonDocumentACLAwareRepositoryInterface
@@ -32,4 +33,11 @@ interface PersonDocumentACLAwareRepositoryInterface
?\DateTimeImmutable $endDate = null,
?string $content = null
): FetchQueryInterface;
public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $period,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null
): FetchQueryInterface;
}

View File

@@ -8,12 +8,14 @@
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
@@ -36,6 +38,8 @@
{{ chill_pagination(pagination) }}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\AccompanyingPeriod" data-entity-id="{{ accompanyingCourse.id }}"></div>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %}
<ul class="record_actions sticky-form-buttons">
<li class="create">

View File

@@ -17,7 +17,14 @@
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div>
{% endif %}
{% elseif context == 'accompanying-period' and person is defined %}
<div>
<span class="badge bg-primary">
{{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
</span>&nbsp;
</div>
{% endif %}
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>

View File

@@ -21,12 +21,14 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
@@ -66,7 +68,8 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
$repository = new PersonDocumentACLAwareRepository(
$this->entityManager,
$centerManager->reveal(),
$authorizationHelper->reveal()
$authorizationHelper->reveal(),
$this->prophesize(Security::class)->reveal()
);
$person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p")
@@ -86,6 +89,62 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
self::assertIsInt($nb, "test that the query could be executed");
}
/**
* @dataProvider provideDateForFetchQueryForAccompanyingPeriod
*/
public function testBuildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $period,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null
): void {
$centerManager = $this->prophesize(CenterResolverManagerInterface::class);
$centerManager->resolveCenters(Argument::type(Person::class))
->willReturn([new Center()]);
$scopes = $this->scopeRepository->findAll();
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes(PersonDocumentVoter::SEE, Argument::any())->willReturn($scopes);
$security = $this->prophesize(Security::class);
$security->isGranted(PersonDocumentVoter::SEE, Argument::type(Person::class))->willReturn(true);
$repository = new PersonDocumentACLAwareRepository(
$this->entityManager,
$centerManager->reveal(),
$authorizationHelper->reveal(),
$security->reveal()
);
$query = $repository->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$nb = $this->entityManager->getConnection()
->fetchOne("SELECT COUNT(*) FROM ({$sql}) AS sq", $params, $types);
self::assertIsInt($nb, "test that the query could be executed");
}
public function provideDateForFetchQueryForAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager->createQuery(
"SELECT p FROM " . AccompanyingPeriod::class . " p WHERE SIZE(p.participations) > 0"
)
->setMaxResults(1)->getSingleResult()) {
throw new \RuntimeException("no course found");
}
yield [$period, null, null, null];
yield [$period, new DateTimeImmutable('1 year ago'), null, null];
yield [$period, null, new DateTimeImmutable('1 year ago'), null];
yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null];
yield [$period, null, null, 'test'];
yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test'];
}
public function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];

View File

@@ -18,6 +18,7 @@ No document found: Aucun document trouvé
The document is successfully registered: Le document est enregistré
The document is successfully updated: Le document est mis à jour
Any description: Aucune description
Document from person %name%: Document de l'usager %name%
See the document: Voir le document
document:
@@ -27,6 +28,7 @@ generic_doc:
filter:
keys:
accompanying_course_document: Document du parcours
person_document: Documents de l'usager
date-range: Date du document
# delete

View File

@@ -72,17 +72,21 @@ final readonly class ExportFormHelper
];
}
$allowedFormatters = $this->exportManager
->getFormattersByTypes($export->getAllowedFormattersTypes());
$choices = [];
foreach (array_keys(iterator_to_array($allowedFormatters)) as $alias) {
$choices[] = $alias;
}
if ($export instanceof ExportInterface) {
$allowedFormatters = $this->exportManager
->getFormattersByTypes($export->getAllowedFormattersTypes());
$choices = [];
foreach (array_keys(iterator_to_array($allowedFormatters)) as $alias) {
$choices[] = $alias;
}
$data[ExportType::PICK_FORMATTER_KEY]['alias'] = match (count($choices)) {
1 => $choices[0],
default => null,
};
$data[ExportType::PICK_FORMATTER_KEY]['alias'] = match (count($choices)) {
1 => $choices[0],
default => null,
};
} else {
unset($data[ExportType::PICK_FORMATTER_KEY]);
}
return $data;
}

View File

@@ -113,8 +113,12 @@ class ExportManager
*
* @return FilterInterface[] a \Generator that contains filters. The key is the filter's alias
*/
public function &getFiltersApplyingOn(ExportInterface $export, ?array $centers = null)
public function &getFiltersApplyingOn(ExportInterface|DirectExportInterface $export, ?array $centers = null): iterable
{
if ($export instanceof DirectExportInterface) {
return;
}
foreach ($this->filters as $alias => $filter) {
if (
in_array($filter->applyOn(), $export->supportsModifiers(), true)
@@ -132,9 +136,9 @@ class ExportManager
*
* @return null|iterable<string, AggregatorInterface> a \Generator that contains aggretagors. The key is the filter's alias
*/
public function &getAggregatorsApplyingOn(ExportInterface $export, ?array $centers = null): ?iterable
public function &getAggregatorsApplyingOn(ExportInterface|DirectExportInterface $export, ?array $centers = null): ?iterable
{
if ($export instanceof ListInterface) {
if ($export instanceof ListInterface || $export instanceof DirectExportInterface) {
return;
}

View File

@@ -271,6 +271,7 @@ class AccompanyingPeriod implements
* cascade={"persist", "refresh", "remove", "merge", "detach"})
* @Groups({"read", "docgen:read"})
* @ParticipationOverlap(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED})
* @var Collection<AccompanyingPeriodParticipation>
*/
private Collection $participations;
@@ -873,6 +874,7 @@ class AccompanyingPeriod implements
/**
* Get Participations Collection.
* @return Collection<AccompanyingPeriodParticipation>
*/
public function getParticipations(): Collection
{

View File

@@ -20,6 +20,7 @@ use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\Helper\UserHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -146,7 +147,9 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
}
public function getFormDefaultData(): array
{
return [];
return [
'calc_date' => new RollingDate(RollingDate::T_TODAY)
];
}
public function getAllowedFormattersTypes()

View File

@@ -0,0 +1,36 @@
<?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 Version20230627130331 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add constraint to force unicity on chill_person_accompanying_period_user_history';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history
ADD CONSTRAINT acc_period_user_history_not_overlaps
EXCLUDE USING GIST (accompanyingperiod_id with =, tsrange(startdate, enddate) with &&)
DEFERRABLE INITIALLY DEFERRED');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history DROP CONSTRAINT acc_period_user_history_not_overlaps');
}
}

View File

@@ -555,22 +555,22 @@ is regular: le parcours est régulier
Intensity: Intensité
Group by intensity: Grouper les parcours par intensité
Filter by active on date: Filtrer les parcours actifs à une date
On date: Actifs à cette date
"Filtered by actives courses: active on %ondate%": "Filtrer les parcours actifs: actifs le %ondate%"
Filter by active on date: Filtrer les parcours ouverts à une date
On date: A l'état ouvert à cette date
"Filtered by actives courses: active on %ondate%": "Filtrer les parcours ouverts: actifs le %ondate%"
Filter by active at least one day between dates: Filtrer les parcours actifs au moins un jour dans la période
"Filtered by actives courses: at least one day between %datefrom% and %dateto%": "Filtrer les parcours actifs: au moins un jour entre le %datefrom% et le %dateto%"
Filter by active at least one day between dates: Filtrer les parcours ouverts au moins un jour dans la période
"Filtered by actives courses: at least one day between %datefrom% and %dateto%": "Filtrer les parcours ouverts: au moins un jour entre le %datefrom% et le %dateto%"
Filter by referrers: Filtrer les parcours par référent
Accepted referrers: Référents
"Filtered by referrer: only %referrers%": "Filtré par référent: uniquement %referrers%"
Group by referrers: Grouper les parcours par référent
Filter by opened between dates: Filtrer les parcours ouverts entre deux dates
Filter by opened between dates: Filtrer les parcours dont la date d'ouverture est comprise entre deux dates
Date from: Date de début
Date to: Date de fin
"Filtered by opening dates: between %datefrom% and %dateto%": "Filtrer les parcours ouverts entre deux dates: entre le %datefrom% et le %dateto%"
"Filtered by opening dates: between %datefrom% and %dateto%": "Filtrer les parcours dont la date d'ouverture est comprise entre le %datefrom% et le %dateto%"
Filter by temporary location: Filtrer les parcours avec une localisation temporaire
Filter by which has no referrer: Filtrer les parcours sans référent
@@ -1241,4 +1241,3 @@ generic_doc:
filter:
keys:
accompanying_period_work_evaluation_document: Document des actions d'accompagnement
person_document: Documents de la personne