Merge remote-tracking branch 'origin/master' into rector/rules-up-to-php80

Conflicts:
	src/Bundle/ChillActivityBundle/Controller/ActivityController.php
	src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php
	src/Bundle/ChillActivityBundle/Menu/PersonMenuBuilder.php
	src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php
	src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php
	src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php
	src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php
	src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
	src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
	src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php
	src/Bundle/ChillEventBundle/Search/EventSearch.php
	src/Bundle/ChillMainBundle/Controller/ExportController.php
	src/Bundle/ChillMainBundle/Controller/PermissionsGroupController.php
	src/Bundle/ChillMainBundle/Cron/CronManager.php
	src/Bundle/ChillMainBundle/Entity/CronJobExecution.php
	src/Bundle/ChillMainBundle/Export/ExportManager.php
	src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php
	src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php
	src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
	src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php
	src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php
	src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php
	src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php
	src/Bundle/ChillPersonBundle/Controller/SocialWorkSocialActionApiController.php
	src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php
	src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php
	src/Bundle/ChillPersonBundle/Export/Export/ListHouseholdInPeriod.php
	src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php
	src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php
	src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php
	src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodWorkEvaluationContext.php
	src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php
	src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php
	src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
This commit is contained in:
2023-07-17 12:49:13 +02:00
544 changed files with 18622 additions and 2105 deletions

View File

@@ -11,8 +11,21 @@ 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;
class ChillDocStoreBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$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

@@ -33,11 +33,23 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class DocumentAccompanyingCourseController extends AbstractController
{
protected AuthorizationHelper $authorizationHelper;
protected EventDispatcherInterface $eventDispatcher;
protected TranslatorInterface $translator;
/**
* DocumentAccompanyingCourseController constructor.
*/
public function __construct(protected TranslatorInterface $translator, protected EventDispatcherInterface $eventDispatcher, protected AuthorizationHelper $authorizationHelper, private PaginatorFactory $paginatorFactory, private AccompanyingCourseDocumentRepository $courseRepository)
{
public function __construct(
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
) {
$this->translator = $translator;
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
}
/**
@@ -62,7 +74,7 @@ class DocumentAccompanyingCourseController extends AbstractController
return $this->redirect($request->query->get('returnPath'));
}
return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]);
return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]);
}
return $this->render(
@@ -116,40 +128,6 @@ class DocumentAccompanyingCourseController extends AbstractController
);
}
/**
* @Route("/", name="accompanying_course_document_index", methods="GET")
*/
public function index(AccompanyingPeriod $course): Response
{
$em = $this->getDoctrine()->getManager();
if (null === $course) {
throw $this->createNotFoundException('Accompanying period not found');
}
$this->denyAccessUnlessGranted(AccompanyingCourseDocumentVoter::SEE, $course);
$total = $this->courseRepository->countByCourse($course);
$pagination = $this->paginatorFactory->create($total);
$documents = $this->courseRepository
->findBy(
['course' => $course],
['date' => 'DESC', 'id' => 'DESC'],
$pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber()
);
return $this->render(
'ChillDocStoreBundle:AccompanyingCourseDocument:index.html.twig',
[
'documents' => $documents,
'accompanyingCourse' => $course,
'pagination' => $pagination,
]
);
}
/**
* @Route("/new", name="accompanying_course_document_new", methods="GET|POST")
*/
@@ -182,7 +160,7 @@ class DocumentAccompanyingCourseController extends AbstractController
$this->addFlash('success', $this->translator->trans('The document is successfully registered'));
return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]);
return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]);
}
if ($form->isSubmitted() && !$form->isValid()) {

View File

@@ -39,11 +39,23 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class DocumentPersonController extends AbstractController
{
protected AuthorizationHelper $authorizationHelper;
protected EventDispatcherInterface $eventDispatcher;
protected TranslatorInterface $translator;
/**
* DocumentPersonController constructor.
*/
public function __construct(protected TranslatorInterface $translator, protected EventDispatcherInterface $eventDispatcher, protected AuthorizationHelper $authorizationHelper, private PaginatorFactory $paginatorFactory, private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository)
{
public function __construct(
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
) {
$this->translator = $translator;
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
}
/**
@@ -68,7 +80,7 @@ class DocumentPersonController extends AbstractController
return $this->redirect($request->query->get('returnPath'));
}
return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]);
return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]);
}
return $this->render(
@@ -140,45 +152,6 @@ class DocumentPersonController extends AbstractController
);
}
/**
* @Route("/", name="person_document_index", methods="GET")
*/
public function index(Person $person): Response
{
$em = $this->getDoctrine()->getManager();
if (null === $person) {
throw $this->createNotFoundException('Person not found');
}
$this->denyAccessUnlessGranted(PersonVoter::SEE, $person);
$total = $this->personDocumentACLAwareRepository->countByPerson($person);
$pagination = $this->paginatorFactory->create($total);
$documents = $this->personDocumentACLAwareRepository->findByPerson(
$person,
['date' => 'DESC', 'id' => 'DESC'],
$pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber()
);
$event = new PrivacyEvent($person, [
'element_class' => PersonDocument::class,
'action' => 'index',
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
return $this->render(
'ChillDocStoreBundle:PersonDocument:index.html.twig',
[
'documents' => $documents,
'person' => $person,
'pagination' => $pagination,
]
);
}
/**
* @Route("/new", name="person_document_new", methods="GET|POST")
*/
@@ -213,7 +186,7 @@ class DocumentPersonController extends AbstractController
$this->addFlash('success', $this->translator->trans('The document is successfully registered'));
return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]);
return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]);
}
if ($form->isSubmitted() && !$form->isValid()) {

View File

@@ -0,0 +1,98 @@
<?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\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Bundle\TwigBundle\TwigEngine;
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 GenericDocForAccompanyingPeriodController
{
public function __construct(
private FilterOrderHelperFactory $filterOrderHelperFactory,
private Manager $manager,
private PaginatorFactory $paginator,
private Security $security,
private EngineInterface $twig,
) {
}
/**
* @param AccompanyingPeriod $accompanyingPeriod
* @return Response
* @throws \Doctrine\DBAL\Exception
*
* @Route("/{_locale}/doc-store/generic-doc/by-period/{id}/index", name="chill_docstore_generic-doc_by-period_index")
*/
public function list(AccompanyingPeriod $accompanyingPeriod): Response
{
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) {
throw new AccessDeniedHttpException("not allowed to see the documents for accompanying period");
}
$filterBuilder = $this->filterOrderHelperFactory
->create(self::class)
->addSearchBox()
->addDateRange('dateRange', 'generic_doc.filter.date-range');
if ([] !== $places = $this->manager->placesForAccompanyingPeriod($accompanyingPeriod)) {
$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->countDocForAccompanyingPeriod(
$accompanyingPeriod,
$startDate,
$endDate,
$content,
$filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
);
$paginator = $this->paginator->create($nb);
$documents = $this->manager->findDocForAccompanyingPeriod(
$accompanyingPeriod,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
$startDate,
$endDate,
$content,
$filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
);
return new Response($this->twig->render(
'@ChillDocStore/GenericDoc/accompanying_period_list.html.twig',
[
'accompanyingCourse' => $accompanyingPeriod,
'pagination' => $paginator,
'documents' => iterator_to_array($documents),
'filter' => $filter,
]
));
}
}

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

@@ -0,0 +1,233 @@
<?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 Doctrine\DBAL\Types\Types;
class FetchQuery implements FetchQueryInterface
{
/**
* @var list<string>
*/
private array $joins = [];
/**
* @var list<list<mixed>>
*/
private array $joinParams = [];
/**
* @var array<list<Types::*>>
*/
private array $joinTypes = [];
/**
* @var array<string>
*/
private array $wheres = [];
/**
* @var array<list<mixed>>
*/
private array $whereParams = [];
/**
* @var array<list<Types::*>>
*/
private array $whereTypes = [];
public function __construct(
private readonly string $selectKeyString,
private readonly string $selectIdentifierJsonB,
private readonly string $selectDate,
private string $from = '',
private array $selectIdentifierParams = [],
private array $selectIdentifierTypes = [],
private array $selectDateParams = [],
private array $selectDateTypes = [],
) {
}
public function addJoinClause(string $sql, array $params = [], array $types = []): int
{
$this->joins[] = $sql;
$this->joinParams[] = $params;
$this->joinTypes[] = $types;
return count($this->joins) - 1;
}
public function addWhereClause(string $sql, array $params = [], array $types = []): int
{
$this->wheres[] = $sql;
$this->whereParams[] = $params;
$this->whereTypes[] = $types;
return count($this->wheres) - 1;
}
public function removeWhereClause(int $index): void
{
if (!array_key_exists($index, $this->wheres)) {
throw new \UnexpectedValueException("this index does not exists");
}
unset($this->wheres[$index], $this->whereParams[$index], $this->whereTypes[$index]);
}
public function removeJoinClause(int $index): void
{
if (!array_key_exists($index, $this->joins)) {
throw new \UnexpectedValueException("this index does not exists");
}
unset($this->joins[$index], $this->joinParams[$index], $this->joinTypes[$index]);
}
public function getSelectKeyString(): string
{
return $this->selectKeyString;
}
public function getSelectIdentifierJsonB(): string
{
return $this->selectIdentifierJsonB;
}
/**
* @inheritDoc
*/
public function getSelectIdentifierParams(): array
{
return $this->selectIdentifierParams;
}
public function getSelectIdentifiersTypes(): array
{
return $this->selectIdentifierTypes;
}
public function getSelectDate(): string
{
return $this->selectDate;
}
public function getSelectDateTypes(): array
{
return $this->selectDateTypes;
}
/**
* @inheritDoc
*/
public function getSelectDateParams(): array
{
return $this->selectDateParams;
}
public function getFromQuery(): string
{
return $this->from . " " . implode(' ', $this->joins);
}
/**
* @inheritDoc
*/
public function getFromQueryParams(): array
{
$result = [];
foreach ($this->joinParams as $params) {
$result = [...$result, ...$params];
}
return $result;
}
public function getFromQueryTypes(): array
{
$result = [];
foreach ($this->joinTypes as $types) {
$result = [...$result, ...$types];
}
return $result;
}
public function getWhereQuery(): string
{
return implode(' AND ', $this->wheres);
}
/**
* @inheritDoc
*/
public function getWhereQueryParams(): array
{
$result = [];
foreach ($this->whereParams as $params) {
$result = [...$result, ...$params];
}
return $result;
}
public function getWhereQueryTypes(): array
{
$result = [];
foreach ($this->whereTypes as $types) {
$result = [...$result, ...$types];
}
return $result;
}
public function setSelectIdentifierParams(array $selectIdentifierParams): self
{
$this->selectIdentifierParams = $selectIdentifierParams;
return $this;
}
public function setSelectDateParams(array $selectDateParams): self
{
$this->selectDateParams = $selectDateParams;
return $this;
}
public function setFrom(string $from): self
{
$this->from = $from;
return $this;
}
public function setSelectIdentifierTypes(array $selectIdentifierTypes): self
{
$this->selectIdentifierTypes = $selectIdentifierTypes;
return $this;
}
public function setSelectDateTypes(array $selectDateTypes): self
{
$this->selectDateTypes = $selectDateTypes;
return $this;
}
}

View File

@@ -0,0 +1,67 @@
<?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 Doctrine\DBAL\Types\Types;
interface FetchQueryInterface
{
public function getSelectKeyString(): string;
public function getSelectIdentifierJsonB(): string;
/**
* @return list<mixed>
*/
public function getSelectIdentifierParams(): array;
/**
* @return list<Types::*>
*/
public function getSelectIdentifiersTypes(): array;
public function getSelectDate(): string;
/**
* @return list<mixed>
*/
public function getSelectDateParams(): array;
/**
* @return list<Types::*>
*/
public function getSelectDateTypes(): array;
public function getFromQuery(): string;
/**
* @return list<mixed>
*/
public function getFromQueryParams(): array;
/**
* @return list<Types::*>
*/
public function getFromQueryTypes(): array;
public function getWhereQuery(): string;
/**
* @return list<mixed>
*/
public function getWhereQueryParams(): array;
/**
* @return list<Types::*>
*/
public function getWhereQueryTypes(): array;
}

View File

@@ -0,0 +1,57 @@
<?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 Doctrine\DBAL\Types\Types;
final readonly class FetchQueryToSqlBuilder
{
private const SQL = <<<'SQL'
SELECT
'{{ key }}' AS key,
{{ identifiers }} AS identifiers,
{{ date }}::date AS doc_date
FROM {{ from }}
{{ where }}
SQL;
/**
* @param FetchQueryInterface $query
* @return array{sql: string, params: list<mixed>, types: list<Types::*>}
*/
public function toSql(FetchQueryInterface $query): array
{
$sql = strtr(self::SQL, [
'{{ key }}' => $query->getSelectKeyString(),
'{{ identifiers }}' => $query->getSelectIdentifierJsonB(),
'{{ date }}' => $query->getSelectDate(),
'{{ from }}' => $query->getFromQuery(),
'{{ where }}' => '' === ($w = $query->getWhereQuery()) ? '' : 'WHERE ' . $w,
]);
$params = [
...$query->getSelectIdentifierParams(),
...$query->getSelectDateParams(),
...$query->getFromQueryParams(),
...$query->getWhereQueryParams()
];
$types = [
...$query->getSelectIdentifiersTypes(),
...$query->getSelectDateTypes(),
...$query->getFromQueryTypes(),
...$query->getWhereQueryTypes(),
];
return ['sql' => $sql, 'params' => $params, 'types' => $types];
}
}

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\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
final readonly class GenericDocDTO
{
public function __construct(
public string $key,
public array $identifiers,
public \DateTimeImmutable $docDate,
public AccompanyingPeriod|Person $linked,
) {
}
public function getContext(): string
{
return $this->linked instanceof AccompanyingPeriod ? 'accompanying-period' : 'person';
}
}

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\AccompanyingPeriod;
interface GenericDocForAccompanyingPeriodProviderInterface
{
public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod,
?\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 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

@@ -0,0 +1,222 @@
<?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\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Types\Types;
final readonly class Manager
{
private FetchQueryToSqlBuilder $builder;
public function __construct(
/**
* @var iterable<GenericDocForAccompanyingPeriodProviderInterface>
*/
private iterable $providersForAccompanyingPeriod,
/**
* @var iterable<GenericDocForPersonProviderInterface>
*/
private iterable $providersForPerson,
private Connection $connection,
) {
$this->builder = new FetchQueryToSqlBuilder();
}
/**
* @param list<string> $places
* @throws Exception
*/
public function countDocForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null,
array $places = []
): 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;
}
$countSql = "SELECT count(*) AS c FROM ({$sql}) AS sq";
$result = $this->connection->executeQuery($countSql, $params, $types);
$number = $result->fetchOne();
if (false === $number) {
throw new \UnexpectedValueException("number of documents failed to load");
}
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(
AccompanyingPeriod $accompanyingPeriod,
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($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 [];
}
$runSql = "{$sql} ORDER BY doc_date DESC LIMIT ? OFFSET ?";
$runParams = [...$params, ...[$limit, $offset]];
$runTypes = [...$types, ...[Types::INTEGER, Types::INTEGER]];
foreach ($this->connection->iterateAssociative($runSql, $runParams, $runTypes) as $row) {
yield new GenericDocDTO(
$row['key'],
json_decode($row['identifiers'], true, 512, JSON_THROW_ON_ERROR),
new \DateTimeImmutable($row['doc_date']),
$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 ORDER BY key";
$keys = [];
foreach ($this->connection->iterateAssociative($runSql, $params, $types) as $k) {
$keys[] = $k['key'];
}
return $keys;
}
/**
* @param list<string> $places places to search. When empty, search in all places
*/
private function buildUnionQuery(
AccompanyingPeriod|Person $linked,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null,
array $places = [],
): array {
$queries = [];
if ($linked instanceof AccompanyingPeriod) {
foreach ($this->providersForAccompanyingPeriod as $provider) {
if (!$provider->isAllowedForAccompanyingPeriod($linked)) {
continue;
}
$queries[] = $provider->buildFetchQueryForAccompanyingPeriod($linked, $startDate, $endDate, $content);
}
} else {
foreach ($this->providersForPerson as $provider) {
if (!$provider->isAllowedForPerson($linked)) {
continue;
}
$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,147 @@
<?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\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseDocumentGenericDocProvider implements GenericDocForAccompanyingPeriodProviderInterface, GenericDocForPersonProviderInterface
{
public const KEY = 'accompanying_course_document';
public function __construct(
private Security $security,
private EntityManagerInterface $entityManager,
) {
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$query = new FetchQuery(
self::KEY,
sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
$classMetadata->getColumnName('date'),
$classMetadata->getSchemaName() . '.' . $classMetadata->getTableName()
);
$query->addWhereClause(
sprintf('%s = ?', $classMetadata->getSingleAssociationJoinColumnName('course')),
[$accompanyingPeriod->getId()],
[Types::INTEGER]
);
return $this->addWhereClause($query, $startDate, $endDate, $content);
}
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
{
return $this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod);
}
public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$query = new FetchQuery(
self::KEY,
sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
$classMetadata->getColumnName('date'),
$classMetadata->getSchemaName() . '.' . $classMetadata->getTableName() . ' AS acc_course_document'
);
$atLeastOne = false;
$or = [];
$orParams = [];
$orTypes = [];
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $participation->getAccompanyingPeriod())) {
continue;
}
$atLeastOne = true;
$or[] = sprintf(
"(acc_course_document.%s = ? AND acc_course_document.%s BETWEEN ? AND COALESCE(?, 'infinity'::date))",
$classMetadata->getSingleAssociationJoinColumnName('course'),
$classMetadata->getColumnName('date')
);
$orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), $participation->getStartDate(), $participation->getEndDate()];
$orTypes = [...$orTypes, Types::INTEGER, Types::DATE_MUTABLE, Types::DATE_MUTABLE];
}
if (!$atLeastOne) {
// there aren't any period allowed to be seen. Add an unreachable condition
$query->addWhereClause('TRUE = FALSE');
return $query;
}
$query->addWhereClause('(' . implode(' OR ', $or) . ')', $orParams, $orTypes);
return $this->addWhereClause($query, $startDate, $endDate, $content);
}
public function isAllowedForPerson(Person $person): bool
{
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
}
private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
if (null !== $startDate) {
$query->addWhereClause(
sprintf('? <= %s', $classMetadata->getColumnName('date')),
[$startDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $endDate) {
$query->addWhereClause(
sprintf('? >= %s', $classMetadata->getColumnName('date')),
[$endDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $content and '' !== $content) {
$query->addWhereClause(
sprintf(
'(%s ilike ? OR %s ilike ?)',
$classMetadata->getColumnName('title'),
$classMetadata->getColumnName('description')
),
['%' . $content . '%', '%' . $content . '%'],
[Types::STRING, Types::STRING]
);
}
return $query;
}
}

View File

@@ -0,0 +1,65 @@
<?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\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, GenericDocForAccompanyingPeriodProviderInterface
{
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);
}
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

@@ -0,0 +1,59 @@
<?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\Renderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
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 === AccompanyingCourseDocumentGenericDocProvider::KEY
|| $genericDocDTO->key === PersonDocumentGenericDocProvider::KEY;
}
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{
return '@ChillDocStore/List/list_item.html.twig';
}
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
{
if (AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key) {
return [
'document' => $doc = $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id']),
'accompanyingCourse' => $doc->getCourse(),
'options' => $options,
'context' => $genericDocDTO->getContext(),
];
}
// this is a person
return [
'document' => $doc = $this->personDocumentRepository->find($genericDocDTO->identifiers['id']),
'person' => $doc->getPerson(),
'options' => $options,
'context' => $genericDocDTO->getContext(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?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\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
final class GenericDocExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('chill_generic_doc_render', [GenericDocExtensionRuntime::class, 'renderGenericDoc'], [
'needs_environment' => true,
'is_safe' => ['html'],
])
];
}
}

View File

@@ -0,0 +1,50 @@
<?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\Twig;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Extension\RuntimeExtensionInterface;
final readonly class GenericDocExtensionRuntime implements RuntimeExtensionInterface
{
public function __construct(
/**
* @var list<GenericDocRendererInterface>
*/
private iterable $renderers,
) {
}
/**
* @throws RuntimeError
* @throws SyntaxError
* @throws LoaderError
*/
public function renderGenericDoc(Environment $twig, GenericDocDTO $genericDocDTO, array $options = []): string
{
foreach ($this->renderers as $renderer) {
if ($renderer->supports($genericDocDTO)) {
return $twig->render(
$renderer->getTemplate($genericDocDTO, $options),
$renderer->getTemplateData($genericDocDTO, $options),
);
}
}
throw new \LogicException("no renderer found");
}
}

View File

@@ -0,0 +1,24 @@
<?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\Twig;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
interface GenericDocRendererInterface
{
public function supports(GenericDocDTO $genericDocDTO, $options = []): bool;
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string;
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array;
}

View File

@@ -45,9 +45,9 @@ final class MenuBuilder implements LocalMenuBuilderInterface
if ($this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $course)) {
$menu->addChild($this->translator->trans('Documents'), [
'route' => 'accompanying_course_document_index',
'route' => 'chill_docstore_generic-doc_by-period_index',
'routeParameters' => [
'course' => $course->getId(),
'id' => $course->getId(),
],
])
->setExtras([
@@ -63,9 +63,9 @@ final class MenuBuilder implements LocalMenuBuilderInterface
if ($this->security->isGranted(PersonDocumentVoter::SEE, $person)) {
$menu->addChild($this->translator->trans('Documents'), [
'route' => 'person_document_index',
'route' => 'chill_docstore_generic-doc_by-person_index',
'routeParameters' => [
'person' => $person->getId(),
'id' => $person->getId(),
],
])
->setExtras([

View File

@@ -12,18 +12,33 @@ 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\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
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
final readonly class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface
{
public function __construct(private EntityManagerInterface $em, private AuthorizationHelperInterface $authorizationHelper, private CenterResolverDispatcher $centerResolverDispatcher, private Security $security)
{
public function __construct(
private EntityManagerInterface $em,
private CenterResolverManagerInterface $centerResolverManager,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
private Security $security,
) {
}
public function buildQueryByPerson(Person $person): QueryBuilder
@@ -37,6 +52,128 @@ 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 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);
$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]
);
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')),
[$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)');
@@ -63,16 +200,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,33 @@ 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
{
/**
* @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;
public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $period,
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null
): FetchQueryInterface;
}

View File

@@ -0,0 +1,57 @@
<?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\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

@@ -44,3 +44,9 @@ async function download_and_open(event: Event): Promise<void> {
}
</script>
<style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@@ -1,51 +1,96 @@
<template>
<a :class="props.classes" @click="download_and_open($event)">
<i class="fa fa-download"></i>
Télécharger
</a>
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
<i class="fa fa-download"></i>
Télécharger
</a>
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
<i class="fa fa-external-link"></i>
Ouvrir
</a>
</template>
<script lang="ts" setup>
import {reactive} from "vue";
import {reactive, ref, nextTick, onMounted} from "vue";
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
import mime from "mime";
import {StoredObject} from "../../types";
interface DownloadButtonConfig {
storedObject: StoredObject,
classes: {[k: string]: boolean},
filename?: string,
storedObject: StoredObject,
classes: { [k: string]: boolean },
filename?: string,
}
interface DownloadButtonState {
content: null|string
is_ready: boolean,
is_running: boolean,
href_url: string,
}
const props = defineProps<DownloadButtonConfig>();
const state: DownloadButtonState = reactive({content: null});
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
const document_name = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
return document_name + '.' + ext;
}
return document_name;
}
async function download_and_open(event: Event): Promise<void> {
const button = event.target as HTMLAnchorElement;
const button = event.target as HTMLAnchorElement;
if (null === state.content) {
event.preventDefault();
if (state.is_running) {
console.log('state is running, aborting');
return;
}
state.is_running = true;
if (state.is_ready) {
console.log('state is ready. This should not happens');
return;
}
const urlInfo = build_download_info_link(props.storedObject.filename);
let raw;
const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
state.content = window.URL.createObjectURL(raw);
button.href = window.URL.createObjectURL(raw);
button.type = props.storedObject.type;
button.download = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
button.download = button.download + '.' + ext;
try {
raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
} catch (e) {
console.error("error while downloading and decrypting document");
console.error(e);
throw e;
}
}
button.click();
console.log('document downloading (and decrypting) successfully');
console.log('creating the url')
state.href_url = window.URL.createObjectURL(raw);
console.log('url created', state.href_url);
state.is_running = false;
state.is_ready = true;
console.log('new button marked as ready');
console.log('will click on button');
console.log('openbutton is now', open_button.value);
await nextTick();
console.log('next tick actions');
console.log('openbutton after next tick', open_button.value);
open_button.value?.click();
console.log('open button should have been clicked');
}
</script>
<style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@@ -40,5 +40,8 @@ async function beforeLeave(event: Event): Promise<true> {
</script>
<style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@@ -149,16 +149,21 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe
}
if (iv.length === 0) {
console.log('returning document immediatly');
return rawResponse.blob();
}
console.log('start decrypting doc');
const rawBuffer = await rawResponse.arrayBuffer();
try {
const key = await window.crypto.subtle
.importKey('jwk', keyData, { name: algo }, false, ['decrypt']);
console.log('key created');
const decrypted = await window.crypto.subtle
.decrypt({ name: algo, iv: iv }, key, rawBuffer);
console.log('doc decrypted');
return Promise.resolve(new Blob([decrypted]));
} catch (e) {

View File

@@ -31,8 +31,8 @@
'title' : 'Delete document ?'|trans,
'display_content' : block('docdescription'),
'confirm_question' : 'Are you sure you want to remove this document ?'|trans,
'cancel_route' : 'accompanying_course_document_index',
'cancel_parameters' : {'course' : accompanyingCourse.id, 'id': document.id},
'cancel_route' : 'chill_docstore_generic-doc_by-period_index',
'cancel_parameters' : {'id' : accompanyingCourse.id},
'form' : delete_form
} ) }}
{% endblock %}

View File

@@ -21,7 +21,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-period_index', {'id': accompanyingCourse.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>
@@ -31,7 +31,7 @@
</li>
{% endif %}
<li class="edit">
<button class="btn btn-edit">{{ 'Edit'|trans }}</button>
<button class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>

View File

@@ -1,52 +0,0 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title %}
{{ 'Documents' }}
{% 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="document-list">
<h1>{{ 'Documents' }}</h1>
{% 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 %}
{% include '@ChillDocStore/List/list_item.html.twig' %}
{% endfor %}
</div>
{% endif %}
{{ 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">
<a href="{{ path('accompanying_course_document_new', {'course': accompanyingCourse.id}) }}" class="btn btn-create">
{{ 'Create'|trans }}
</a>
</li>
</ul>
{% endif %}
</div>
{% endblock %}

View File

@@ -25,7 +25,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-period_index', {'id': accompanyingCourse.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>

View File

@@ -46,7 +46,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-period_index', {'id': accompanyingCourse.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>

View File

@@ -1,5 +1,5 @@
{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%}
<div
<div class="d-inline-flex"
data-download-buttons
data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
data-can-edit="{{ can_edit ? '1' : '0' }}"

View File

@@ -0,0 +1,54 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title %}
{{ 'Documents' }}
{% 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_script_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="document-list">
<h1>{{ 'Documents' }}</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\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">
<a href="{{ path('accompanying_course_document_new', {'course': accompanyingCourse.id}) }}" class="btn btn-create">
{{ 'Create'|trans }}
</a>
</li>
</ul>
{% endif %}
</div>
{% endblock %}

View File

@@ -44,12 +44,14 @@
<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 %}
{% include 'ChillDocStoreBundle:List:list_item.html.twig' %}
{{ document|chill_generic_doc_render }}
{% endfor %}
</div>
{% endif %}

View File

@@ -10,8 +10,23 @@
{% elseif document.object.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
{% if context == 'person' and accompanyingCourse is defined %}
<div>
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div>
{% 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 }}
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.object.type is not empty %}
<div>

View File

@@ -36,8 +36,8 @@
'title' : 'Delete document ?'|trans,
'display_content' : block('docdescription'),
'confirm_question' : 'Are you sure you want to remove this document ?'|trans,
'cancel_route' : 'person_document_index',
'cancel_parameters' : {'person' : person.id, 'id': document.id},
'cancel_route' : 'chill_docstore_generic-doc_by-person_index',
'cancel_parameters' : {'id' : person.id},
'form' : delete_form
} ) }}
{% endblock %}

View File

@@ -38,7 +38,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('person_document_index', {'person': person.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-person_index', {'id': person.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>

View File

@@ -42,7 +42,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('person_document_index', {'person': person.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-person_index', {'id': person.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>

View File

@@ -63,7 +63,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('person_document_index', {'person': person.id}) }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_docstore_generic-doc_by-person_index', {'id': person.id}) }}" class="btn btn-cancel">
{{ 'Back to the list' | trans }}
</a>
</li>

View File

@@ -0,0 +1,89 @@
<?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;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Doctrine\DBAL\Types\Types;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
class FetchQueryToSqlBuilderTest extends KernelTestCase
{
public function testToSql(): void
{
$query = new FetchQuery(
'test',
'jsonb_build_object(\'id\', a.column)',
'a.datecolumn',
'my_table a'
);
$query->addJoinClause('LEFT JOIN other b ON a.id = b.foreign_id', ['foo'], [Types::STRING]);
$index = $query->addJoinClause('LEFT JOIN other c ON a.id = c.foreign_id', ['bar'], [Types::STRING]);
$query->addJoinClause('LEFT JOIN other d ON a.id = d.foreign_id', ['bar_baz'], [Types::STRING]);
$query->removeJoinClause($index);
$query->addWhereClause('b.item = ?', ['baz'], [Types::STRING]);
$index = $query->addWhereClause('b.cancel', [ 'foz'], [Types::STRING]);
$query->removeWhereClause($index);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$filteredSql =
implode(" ", array_filter(
explode(" ", str_replace("\n", "", $sql)),
fn (string $tok) => $tok !== ""
))
;
self::assertEquals(
"SELECT 'test' AS key, jsonb_build_object('id', a.column) AS identifiers, ".
"a.datecolumn::date AS doc_date FROM my_table a LEFT JOIN other b ON a.id = b.foreign_id LEFT JOIN other d ON a.id = d.foreign_id WHERE b.item = ?",
$filteredSql
);
self::assertEquals(['foo', 'bar_baz', 'baz'], $params);
self::assertEquals([Types::STRING, Types::STRING, Types::STRING], $types);
}
public function testToSqlWithoutWhere(): void
{
$query = new FetchQuery(
'test',
'jsonb_build_object(\'id\', a.column)',
'a.datecolumn',
'my_table a'
);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$filteredSql =
implode(" ", array_filter(
explode(" ", str_replace("\n", "", $sql)),
fn (string $tok) => $tok !== ""
))
;
self::assertEquals(
"SELECT 'test' AS key, jsonb_build_object('id', a.column) AS identifiers, ".
"a.datecolumn::date AS doc_date FROM my_table a",
$filteredSql
);
self::assertEquals([], $params);
self::assertEquals([], $types);
}
}

View File

@@ -0,0 +1,217 @@
<?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;
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;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
class ManagerTest extends KernelTestCase
{
use ProphecyTrait;
private EntityManagerInterface $em;
private Connection $connection;
protected function setUp(): void
{
self::bootKernel();
$this->em = self::$container->get(EntityManagerInterface::class);
$this->connection = self::$container->get(Connection::class);
}
public function testCountByAccompanyingPeriod(): 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,
);
$nb = $manager->countDocForAccompanyingPeriod($period);
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')
->setMaxResults(1)
->getSingleResult();
if (null === $period) {
throw new \UnexpectedValueException("period not found");
}
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
$this->connection,
);
foreach ($manager->findDocForAccompanyingPeriod($period) as $doc) {
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 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_dummy',
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 isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
{
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,79 @@
<?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\AccompanyingCourseDocumentGenericDocProvider;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
class AccompanyingCourseDocumentGenericDocProviderTest extends KernelTestCase
{
use ProphecyTrait;
private EntityManagerInterface $entityManager;
public function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
/**
* @dataProvider provideSearchArguments
*/
public function testWithoutAnyArgument(?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content = null): void
{
$period = $this->entityManager->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
->setMaxResults(1)
->getSingleResult();
if (null === $period) {
throw new \UnexpectedValueException("period not found");
}
$security = $this->prophesize(Security::class);
$security->isGranted(AccompanyingCourseDocumentVoter::SEE, $period)
->willReturn(true);
$provider = new AccompanyingCourseDocumentGenericDocProvider(
$security->reveal(),
$this->entityManager
);
$query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$nb = $this->entityManager->getConnection()->executeQuery('SELECT COUNT(*) FROM ('.$sql.') AS sq', $params, $types)
->fetchOne();
self::assertIsInt($nb);
}
public function provideSearchArguments(): iterable
{
yield [null, null, null];
yield [new \DateTimeImmutable('1 month ago'), null, null];
yield [new \DateTimeImmutable('1 month ago'), new \DateTimeImmutable('now'), null];
yield [null, null, 'test'];
}
}

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,158 @@
<?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\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
* @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(),
$this->prophesize(Security::class)->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");
}
/**
* @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];
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

@@ -43,8 +43,7 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$course = $this->getRelatedEntity($entityWorkflow)
->getCourse();
$course = $this->getRelatedEntity($entityWorkflow)?->getCourse();
$persons = [];
if (null !== $course) {

View File

@@ -2,46 +2,72 @@ parameters:
# cl_chill_person.example.class: Chill\PersonBundle\Example
services:
Chill\DocStoreBundle\Repository\:
autowire: true
autoconfigure: true
resource: "../Repository/"
Chill\DocStoreBundle\Repository\:
autowire: true
autoconfigure: true
resource: "../Repository/"
Chill\DocStoreBundle\Form\DocumentCategoryType:
class: Chill\DocStoreBundle\Form\DocumentCategoryType
arguments: ["%kernel.bundles%"]
tags:
- { name: form.type }
Chill\DocStoreBundle\Form\DocumentCategoryType:
class: Chill\DocStoreBundle\Form\DocumentCategoryType
arguments: [ "%kernel.bundles%" ]
tags:
- { name: form.type }
Chill\DocStoreBundle\Form\PersonDocumentType:
class: Chill\DocStoreBundle\Form\PersonDocumentType
autowire: true
autoconfigure: true
# arguments:
# - "@chill.main.helper.translatable_string"
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
Chill\DocStoreBundle\Form\PersonDocumentType:
class: Chill\DocStoreBundle\Form\PersonDocumentType
autowire: true
autoconfigure: true
# arguments:
# - "@chill.main.helper.translatable_string"
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
Chill\DocStoreBundle\Security\Authorization\:
resource: "./../Security/Authorization"
autowire: true
autoconfigure: true
tags:
- { name: chill.role }
Chill\DocStoreBundle\Security\Authorization\:
resource: "./../Security/Authorization"
autowire: true
autoconfigure: true
tags:
- { name: chill.role }
Chill\DocStoreBundle\Workflow\:
resource: './../Workflow/'
autoconfigure: true
autowire: true
Chill\DocStoreBundle\Workflow\:
resource: './../Workflow/'
autoconfigure: true
autowire: true
Chill\DocStoreBundle\Serializer\Normalizer\:
autowire: true
resource: '../Serializer/Normalizer/'
tags:
- { name: 'serializer.normalizer', priority: 16 }
Chill\DocStoreBundle\Serializer\Normalizer\:
autowire: true
resource: '../Serializer/Normalizer/'
tags:
- { name: 'serializer.normalizer', priority: 16 }
Chill\DocStoreBundle\Service\:
autowire: true
autoconfigure: true
resource: '../Service/'
Chill\DocStoreBundle\Service\:
autowire: true
autoconfigure: true
resource: '../Service/'
Chill\DocStoreBundle\GenericDoc\Manager:
autowire: true
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
autowire: true
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtensionRuntime:
autoconfigure: true
autowire: true
arguments:
$renderers: !tagged_iterator chill_doc_store.generic_doc_renderer
Chill\DocStoreBundle\GenericDoc\Providers\:
autowire: true
autoconfigure: true
resource: '../GenericDoc/Providers/'
Chill\DocStoreBundle\GenericDoc\Renderer\:
autowire: true
autoconfigure: true
resource: '../GenericDoc/Renderer/'

View File

@@ -18,10 +18,19 @@ 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:
Any title: Aucun titre
generic_doc:
filter:
keys:
accompanying_course_document: Document du parcours
person_document: Documents de l'usager
date-range: Date du document
# delete
Delete document ?: Supprimer le document ?
Are you sure you want to remove this document ?: Êtes-vous sûr·e de vouloir supprimer ce document ?