[generic doc] listing generic doc for person

This commit is contained in:
Julien Fastré 2023-05-30 20:48:35 +02:00
parent eb107f5a15
commit 40af1e64ac
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
15 changed files with 842 additions and 42 deletions

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -22,6 +23,8 @@ class ChillDocStoreBundle extends Bundle
{
$container->registerForAutoconfiguration(GenericDocForAccompanyingPeriodProviderInterface::class)
->addTag('chill_doc_store.generic_doc_accompanying_period_provider');
$container->registerForAutoconfiguration(GenericDocForPersonProviderInterface::class)
->addTag('chill_doc_store.generic_doc_person_provider');
$container->registerForAutoconfiguration(GenericDocRendererInterface::class)
->addTag('chill_doc_store.generic_doc_renderer');
}

View File

@ -0,0 +1,95 @@
<?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\DocStoreBundle\Controller;
use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
final readonly class GenericDocForPerson
{
public function __construct(
private FilterOrderHelperFactory $filterOrderHelperFactory,
private Manager $manager,
private PaginatorFactory $paginator,
private Security $security,
private EngineInterface $twig,
) {
}
/**
* @throws \Doctrine\DBAL\Exception
*
* @Route("/{_locale}/doc-store/generic-doc/by-person/{id}/index", name="chill_docstore_generic-doc_by-person_index")
*/
public function list(Person $person): Response
{
if (!$this->security->isGranted(PersonDocumentVoter::SEE, $person)) {
throw new AccessDeniedHttpException("not allowed to see the documents for person");
}
$filterBuilder = $this->filterOrderHelperFactory
->create(self::class)
->addSearchBox()
->addDateRange('dateRange', 'generic_doc.filter.date-range');
if ([] !== $places = $this->manager->placesForPerson($person)) {
$filterBuilder->addCheckbox('places', $places, [], array_map(
static fn (string $k) => 'generic_doc.filter.keys.' . $k,
$places
));
}
$filter = $filterBuilder
->build();
['to' => $endDate, 'from' => $startDate ] = $filter->getDateRangeData('dateRange');
$content = $filter->getQueryString();
$nb = $this->manager->countDocForPerson(
$person,
$startDate,
$endDate,
$content,
$filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
);
$paginator = $this->paginator->create($nb);
$documents = $this->manager->findDocForPerson(
$person,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
$startDate,
$endDate,
$content,
$filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
);
return new Response($this->twig->render(
'@ChillDocStore/GenericDoc/person_list.html.twig',
[
'person' => $person,
'pagination' => $paginator,
'documents' => iterator_to_array($documents),
'filter' => $filter,
]
));
}
}

View File

@ -25,8 +25,6 @@ interface GenericDocForAccompanyingPeriodProviderInterface
/**
* Return true if the user is allowed to see some documents for this provider.
*
* @return bool
*/
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool;

View File

@ -0,0 +1,31 @@
<?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\DocStoreBundle\GenericDoc;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
interface GenericDocForPersonProviderInterface
{
public function buildFetchQueryForPerson(
Person $person,
?DateTimeImmutable $startDate = null,
?DateTimeImmutable $endDate = null,
?string $content = null,
?string $origin = null
): FetchQueryInterface;
/**
* Return true if the user is allowed to see some documents for this provider.
*/
public function isAllowedForPerson(Person $person): bool;
}

View File

@ -17,16 +17,21 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Types\Types;
class Manager
final readonly class Manager
{
private readonly FetchQueryToSqlBuilder $builder;
private FetchQueryToSqlBuilder $builder;
public function __construct(
/**
* @var iterable<GenericDocForAccompanyingPeriodProviderInterface>
*/
private readonly iterable $providersForAccompanyingPeriod,
private readonly Connection $connection,
private iterable $providersForAccompanyingPeriod,
/**
* @var iterable<GenericDocForPersonProviderInterface>
*/
private iterable $providersForPerson,
private Connection $connection,
) {
$this->builder = new FetchQueryToSqlBuilder();
}
@ -44,6 +49,11 @@ class Manager
): int {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
return $this->countDoc($sql, $params, $types);
}
private function countDoc(string $sql, array $params, array $types): int
{
if ($sql === '') {
return 0;
}
@ -60,8 +70,21 @@ class Manager
return $number;
}
public function countDocForPerson(
Person $person,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null,
array $places = []
): int {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);
return $this->countDoc($sql, $params, $types);
}
/**
* @param list<string> $places places to search. When empty, search in all places
* @return iterable<GenericDocDTO>
* @throws Exception
*/
public function findDocForAccompanyingPeriod(
@ -75,6 +98,15 @@ class Manager
): iterable {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
return $this->findDocs($accompanyingPeriod, $sql, $params, $types, $offset, $limit);
}
/**
* @throws \JsonException
* @throws Exception
*/
private function findDocs(AccompanyingPeriod|Person $linked, string $sql, array $params, array $types, int $offset, int $limit): iterable
{
if ($sql === '') {
return [];
}
@ -88,20 +120,50 @@ class Manager
$row['key'],
json_decode($row['identifiers'], true, 512, JSON_THROW_ON_ERROR),
new \DateTimeImmutable($row['doc_date']),
$accompanyingPeriod,
$linked,
);
}
}
/**
* @param list<string> $places places to search. When empty, search in all places
* @return iterable<GenericDocDTO>
*/
public function findDocForPerson(
Person $person,
int $offset = 0,
int $limit = 20,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null,
array $places = []
): iterable {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);
return $this->findDocs($person, $sql, $params, $types, $offset, $limit);
}
public function placesForPerson(Person $person): array
{
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person);
return $this->places($sql, $params, $types);
}
public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array
{
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod);
return $this->places($sql, $params, $types);
}
private function places(string $sql, array $params, array $types): array
{
if ($sql === '') {
return [];
}
$runSql = "SELECT DISTINCT key FROM ({$sql}) AS sq";
$runSql = "SELECT DISTINCT key FROM ({$sql}) AS sq ORDER BY key";
$keys = [];
@ -122,28 +184,38 @@ class Manager
?string $content = null,
array $places = [],
): array {
$sql = [];
$params = [];
$types = [];
$queries = [];
if ($linked instanceof AccompanyingPeriod) {
foreach ($this->providersForAccompanyingPeriod as $provider) {
if (!$provider->isAllowedForAccompanyingPeriod($linked)) {
continue;
}
$query = $provider->buildFetchQueryForAccompanyingPeriod($linked, $startDate, $endDate, $content);
if ([] !== $places and !in_array($query->getSelectKeyString(), $places, true)) {
$queries[] = $provider->buildFetchQueryForAccompanyingPeriod($linked, $startDate, $endDate, $content);
}
} else {
foreach ($this->providersForPerson as $provider) {
if (!$provider->isAllowedForPerson($linked)) {
continue;
}
['sql' => $q, 'params' => $p, 'types' => $t ] = $this->builder->toSql($query);
$sql[] = $q;
$params = [...$params, ...$p];
$types = [...$types, ...$t];
$queries[] = $provider->buildFetchQueryForPerson($linked, $startDate, $endDate, $content);
}
}
$sql = [];
$params = [];
$types = [];
foreach ($queries as $query) {
if ([] !== $places and !in_array($query->getSelectKeyString(), $places, true)) {
continue;
}
['sql' => $q, 'params' => $p, 'types' => $t ] = $this->builder->toSql($query);
$sql[] = $q;
$params = [...$params, ...$p];
$types = [...$types, ...$t];
}
return ['sql' => implode(' UNION ', $sql), 'params' => $params, 'types' => $types];
}

View File

@ -0,0 +1,51 @@
<?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\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Symfony\Component\Security\Core\Security;
final readonly class PersonDocumentGenericDocProvider implements GenericDocForPersonProviderInterface
{
public const KEY = 'person_document';
public function __construct(
private Security $security,
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository,
) {
}
public function buildFetchQueryForPerson(
Person $person,
?DateTimeImmutable $startDate = null,
?DateTimeImmutable $endDate = null,
?string $content = null,
?string $origin = null
): FetchQueryInterface {
return $this->personDocumentACLAwareRepository->buildFetchQueryForPerson(
$person,
$startDate,
$endDate,
$content
);
}
public function isAllowedForPerson(Person $person): bool
{
return $this->security->isGranted(PersonDocumentVoter::SEE, $person);
}
}

View File

@ -12,20 +12,25 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc\Renderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingProviderCourseDocumentGenericDoc;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface
{
public function __construct(
private AccompanyingCourseDocumentRepository $accompanyingCourseDocumentRepository,
private PersonDocumentRepository $personDocumentRepository,
) {
}
public function supports(GenericDocDTO $genericDocDTO, $options = []): bool
{
return $genericDocDTO->key === AccompanyingProviderCourseDocumentGenericDoc::KEY;
return $genericDocDTO->key === AccompanyingProviderCourseDocumentGenericDoc::KEY
|| $genericDocDTO->key === PersonDocumentGenericDocProvider::KEY;
}
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
@ -35,9 +40,18 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
{
if ($genericDocDTO->linked instanceof AccompanyingPeriod) {
return [
'document' => $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id']),
'accompanyingCourse' => $genericDocDTO->linked,
'options' => $options,
];
}
// this is a person
return [
'document' => $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id']),
'accompanyingCourse' => $genericDocDTO->linked,
'document' => $this->personDocumentRepository->find($genericDocDTO->identifiers['id']),
'person' => $genericDocDTO->linked,
'options' => $options,
];
}

View File

@ -12,30 +12,36 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
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\Person;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface
{
private AuthorizationHelperInterface $authorizationHelper;
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
private CenterResolverDispatcher $centerResolverDispatcher;
private CenterResolverManagerInterface $centerResolverManager;
private EntityManagerInterface $em;
private Security $security;
public function __construct(EntityManagerInterface $em, AuthorizationHelperInterface $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher, Security $security)
public function __construct(EntityManagerInterface $em, CenterResolverManagerInterface $centerResolverManager, AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser)
{
$this->em = $em;
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->security = $security;
$this->centerResolverManager = $centerResolverManager;
$this->authorizationHelperForCurrentUser = $authorizationHelperForCurrentUser;
}
public function buildQueryByPerson(Person $person): QueryBuilder
@ -49,6 +55,62 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
return $qb;
}
public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface
{
$query = $this->buildBaseFetchQueryForPerson($person, $startDate, $endDate, $content);
return $this->addFetchQueryByPersonACL($query, $person);
}
public function buildBaseFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::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->addWhereClause(
sprintf('person_document.%s = ?', $personDocMetadata->getSingleAssociationJoinColumnName('person')),
[$person->getId()],
[Types::INTEGER]
);
if (null !== $startDate) {
$query->addWhereClause(
sprintf('? <= %s', $personDocMetadata->getColumnName('date')),
[$startDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $endDate) {
$query->addWhereClause(
sprintf('? >= %s', $personDocMetadata->getColumnName('date')),
[$endDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $content and '' !== $content) {
$query->addWhereClause(
sprintf(
'(%s ilike ? OR %s ilike ?)',
$personDocMetadata->getColumnName('title'),
$personDocMetadata->getColumnName('description')
),
['%' . $content . '%', '%' . $content . '%'],
[Types::STRING, Types::STRING]
);
}
return $query;
}
public function countByPerson(Person $person): int
{
$qb = $this->buildQueryByPerson($person)->select('COUNT(d)');
@ -75,16 +137,58 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
private function addACL(QueryBuilder $qb, Person $person): void
{
$center = $this->centerResolverDispatcher->resolveCenter($person);
$reachableScopes = [];
$reachableScopes = $this->authorizationHelper
->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::SEE,
$center
);
foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
$reachableScopes = [
...$reachableScopes,
...$this->authorizationHelperForCurrentUser
->getReachableScopes(
PersonDocumentVoter::SEE,
$center
)
];
}
if ([] === $reachableScopes) {
$qb->andWhere("'FALSE' = 'TRUE'");
return;
}
$qb->andWhere($qb->expr()->in('d.scope', ':scopes'))
->setParameter('scopes', $reachableScopes);
}
private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
$reachableScopes = [];
foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
$reachableScopes = [
...$reachableScopes,
...$this->authorizationHelperForCurrentUser->getReachableScopes(PersonDocumentVoter::SEE, $center)
];
}
if ([] === $reachableScopes) {
$fetchQuery->addWhereClause('FALSE = TRUE');
return $fetchQuery;
}
$fetchQuery->addWhereClause(
sprintf(
'person_document.%s IN (%s)',
$personDocMetadata->getSingleAssociationJoinColumnName('scope'),
implode(', ', array_fill(0, count($reachableScopes), '?'))
),
array_map(static fn (Scope $s) => $s->getId(), $reachableScopes),
array_fill(0, count($reachableScopes), Types::INTEGER)
);
return $fetchQuery;
}
}

View File

@ -11,11 +11,25 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\PersonBundle\Entity\Person;
interface PersonDocumentACLAwareRepositoryInterface
{
/**
* @deprecated use fetch query for listing and counting person documents
*/
public function countByPerson(Person $person): int;
/**
* @deprecated use fetch query for listing and counting person documents
*/
public function findByPerson(Person $person, array $orderBy = [], int $limit = 20, int $offset = 0): array;
public function buildFetchQueryForPerson(
Person $person,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null
): FetchQueryInterface;
}

View File

@ -0,0 +1,49 @@
<?php
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use UnexpectedValueException;
/**
* @template ObjectRepository<PersonDocument::class>
*/
readonly class PersonDocumentRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(
private EntityManagerInterface $entityManager
)
{
$this->repository = $this->entityManager->getRepository($this->getClassName());
}
public function find($id): ?PersonDocument
{
return $this->repository->find($id);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?PersonDocument
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return PersonDocument::class;
}
}

View File

@ -0,0 +1,74 @@
{#
* Copyright (C) 2018, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% block title %}
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
{% endblock %}
{% 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_link_tags('mod_docgen_picktemplate') }}
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block content %}
<div class="col-md-10 col-xxl">
<h1>{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}</h1>
{{ filter|chill_render_filter_order_helper }}
{% if documents|length == 0 %}
<p class="chill-no-data-statement">{{ 'No documents'|trans }}</p>
{% else %}
<div class="flex-table chill-task-list">
{% for document in documents %}
{{ document|chill_generic_doc_render }}
{% endfor %}
</div>
{% endif %}
{{ chill_pagination(pagination) }}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\Person" data-entity-id="{{ person.id }}"></div>
{% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %}
<ul class="record_actions sticky-form-buttons">
<li class="create">
<a href="{{ path('person_document_new', {'person': person.id}) }}" class="btn btn-create">
{{ 'Create new document' | trans }}
</a>
</li>
</ul>
{% endif %}
</div>
{% endblock %}

View File

@ -14,9 +14,12 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
@ -53,7 +56,8 @@ class ManagerTest extends KernelTestCase
}
$manager = new Manager(
[new SimpleGenericDocProvider()],
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
@ -62,6 +66,27 @@ class ManagerTest extends KernelTestCase
self::assertIsInt($nb);
}
public function testCountByPerson(): void
{
$person = $this->em->createQuery('SELECT a FROM '.Person::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $person) {
throw new \UnexpectedValueException("person found");
}
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
$nb = $manager->countDocForPerson($person);
self::assertIsInt($nb);
}
public function testFindDocByAccompanyingPeriod(): void
{
$period = $this->em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
@ -73,7 +98,8 @@ class ManagerTest extends KernelTestCase
}
$manager = new Manager(
[new SimpleGenericDocProvider()],
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
@ -81,14 +107,77 @@ class ManagerTest extends KernelTestCase
self::assertInstanceOf(GenericDocDTO::class, $doc);
}
}
public function testFindDocByPerson(): void
{
$person = $this->em->createQuery('SELECT a FROM '.Person::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $person) {
throw new \UnexpectedValueException("person not found");
}
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
foreach ($manager->findDocForPerson($person) as $doc) {
self::assertInstanceOf(GenericDocDTO::class, $doc);
}
}
public function testPlacesForPerson(): void
{
$person = $this->em->createQuery('SELECT a FROM '.Person::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $person) {
throw new \UnexpectedValueException("person not found");
}
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
$places = $manager->placesForPerson($person);
self::assertEquals(['dummy_person_doc'], $places);
}
public function testPlacesForAccompanyingPeriod(): void
{
$period = $this->em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $period) {
throw new \UnexpectedValueException("period not found");
}
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
$places = $manager->placesForAccompanyingPeriod($period);
self::assertEquals(['accompanying_course_document_dummy'], $places);
}
}
final readonly class SimpleGenericDocProvider implements GenericDocForAccompanyingPeriodProviderInterface
final readonly class SimpleGenericDocAccompanyingPeriodProvider implements GenericDocForAccompanyingPeriodProviderInterface
{
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$query = new FetchQuery(
'accompanying_course_document',
'accompanying_course_document_dummy',
sprintf('jsonb_build_object(\'id\', %s)', 'id'),
'd',
'(VALUES (1, \'2023-05-01\'::date), (2, \'2023-05-01\'::date)) AS sq (id, d)',
@ -104,3 +193,25 @@ final readonly class SimpleGenericDocProvider implements GenericDocForAccompanyi
return true;
}
}
final readonly class SimpleGenericDocPersonProvider implements GenericDocForPersonProviderInterface
{
public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$query = new FetchQuery(
'dummy_person_doc',
sprintf('jsonb_build_object(\'id\', %s)', 'id'),
'd',
'(VALUES (1, \'2023-05-01\'::date), (2, \'2023-05-01\'::date)) AS sq (id, d)',
);
$query->addWhereClause('d > ?', [new \DateTimeImmutable('2023-01-01')], [Types::DATE_IMMUTABLE]);
return $query;
}
public function isAllowedForPerson(Person $person): bool
{
return true;
}
}

View File

@ -0,0 +1,84 @@
<?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\DocStoreBundle\Tests\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
class PersonDocumentGenericDocProviderTest extends KernelTestCase
{
use ProphecyTrait;
private EntityManagerInterface $entityManager;
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
$this->personDocumentACLAwareRepository = self::$container->get(PersonDocumentACLAwareRepositoryInterface::class);
}
/**
* @dataProvider provideDataBuildFetchQueryForPerson
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function testBuildFetchQueryForPerson(?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?string $content): void
{
$security = $this->prophesize(Security::class);
$person = $this->entityManager->createQuery('SELECT a FROM '.Person::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $person) {
throw new \UnexpectedValueException("person found");
}
$provider = new PersonDocumentGenericDocProvider(
$security->reveal(),
$this->personDocumentACLAwareRepository
);
$query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$nb = $this->entityManager->getConnection()
->fetchOne("SELECT COUNT(*) AS nb FROM (${sql}) AS sq", $params, $types);
self::assertIsInt($nb, "Test that the query is syntactically correct");
}
public function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new DateTimeImmutable('1 year ago'), null, null];
yield [null, new DateTimeImmutable('1 year ago'), null];
yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test'];
}
}

View File

@ -0,0 +1,99 @@
<?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\DocStoreBundle\Tests\Repository;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
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\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
{
use ProphecyTrait;
private EntityManagerInterface $entityManager;
private ScopeRepositoryInterface $scopeRepository;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
$this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
}
/**
* @dataProvider provideDataBuildFetchQueryForPerson
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function testBuildFetchQueryForPerson(?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);
$repository = new PersonDocumentACLAwareRepository(
$this->entityManager,
$centerManager->reveal(),
$authorizationHelper->reveal()
);
$person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p")
->setMaxResults(1)
->getSingleResult();
if (null === $person) {
throw new \RuntimeException("person not exists in database");
}
$query = $repository->buildFetchQueryForPerson($person, $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 provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new DateTimeImmutable('1 year ago'), null, null];
yield [null, new DateTimeImmutable('1 year ago'), null];
yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test'];
}
}

View File

@ -50,6 +50,7 @@ services:
autoconfigure: true
arguments:
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension:
autoconfigure: true