Merge branch 'calendar/finalization' into testing

This commit is contained in:
Julien Fastré 2022-11-28 14:54:26 +01:00
commit e71bb5d77b
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
28 changed files with 945 additions and 102 deletions

View File

@ -15,6 +15,7 @@ use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Form\CalendarType;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
@ -147,6 +148,8 @@ class CalendarController extends AbstractController
*/
public function editAction(Calendar $entity, Request $request): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::EDIT, $entity);
if (!$this->remoteCalendarConnector->isReady()) {
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
}
@ -208,6 +211,8 @@ class CalendarController extends AbstractController
*/
public function listActionByCourse(AccompanyingPeriod $accompanyingPeriod): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $accompanyingPeriod);
$filterOrder = $this->buildListFilterOrder();
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
@ -228,6 +233,7 @@ class CalendarController extends AbstractController
'accompanyingCourse' => $accompanyingPeriod,
'paginator' => $paginator,
'filterOrder' => $filterOrder,
'nbIgnored' => $this->calendarACLAwareRepository->countIgnoredByAccompanyingPeriod($accompanyingPeriod, $from, $to),
'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class),
]);
}
@ -239,6 +245,8 @@ class CalendarController extends AbstractController
*/
public function listActionByPerson(Person $person): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $person);
$filterOrder = $this->buildListFilterOrder();
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
@ -259,6 +267,7 @@ class CalendarController extends AbstractController
'person' => $person,
'paginator' => $paginator,
'filterOrder' => $filterOrder,
'nbIgnored' => $this->calendarACLAwareRepository->countIgnoredByPerson($person, $from, $to),
'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class),
]);
}
@ -309,7 +318,7 @@ class CalendarController extends AbstractController
$view = '@ChillCalendar/Calendar/newByAccompanyingCourse.html.twig';
$entity->setAccompanyingPeriod($accompanyingPeriod);
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]);
} elseif ($person) {
} elseif (null !== $person) {
$view = '@ChillCalendar/Calendar/newByPerson.html.twig';
$entity->setPerson($person)->addPerson($person);
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]);
@ -319,6 +328,8 @@ class CalendarController extends AbstractController
$entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser')));
}
$this->denyAccessUnlessGranted(CalendarVoter::CREATE, $entity);
$form = $this->createForm(CalendarType::class, $entity)
->add('save', SubmitType::class);
@ -442,6 +453,8 @@ class CalendarController extends AbstractController
*/
public function toActivity(Request $request, Calendar $calendar): RedirectResponse
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $calendar);
$personsId = array_map(
static fn (Person $p): int => $p->getId(),
$calendar->getPersons()->toArray()

View File

@ -12,12 +12,20 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Form\CalendarDocCreateType;
use Chill\CalendarBundle\Form\CalendarDocEditType;
use Chill\CalendarBundle\Security\Voter\CalendarDocVoter;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@ -32,18 +40,202 @@ class CalendarDocController
private EngineInterface $engine;
private EntityManagerInterface $entityManager;
private FormFactoryInterface $formFactory;
private Security $security;
private SerializerInterface $serializer;
private UrlGeneratorInterface $urlGenerator;
public function __construct(Security $security, DocGeneratorTemplateRepository $docGeneratorTemplateRepository, UrlGeneratorInterface $urlGenerator, EngineInterface $engine)
{
$this->security = $security;
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EngineInterface $engine,
EntityManagerInterface $entityManager,
FormFactoryInterface $formFactory,
Security $security,
UrlGeneratorInterface $urlGenerator
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->urlGenerator = $urlGenerator;
$this->engine = $engine;
$this->entityManager = $entityManager;
$this->formFactory = $formFactory;
$this->security = $security;
$this->urlGenerator = $urlGenerator;
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/new", name="chill_calendar_calendardoc_new")
*/
public function create(Calendar $calendar, Request $request): Response
{
$calendarDoc = (new CalendarDoc($calendar, null))->setCalendar($calendar);
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException();
}
// set variables
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/new_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/new_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
default:
throw new UnexpectedValueException('Unsupported context');
}
$calendarDocDTO = new CalendarDoc\CalendarDocCreateDTO();
$form = $this->formFactory->create(CalendarDocCreateType::class, $calendarDocDTO);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$calendarDoc->createFromDTO($calendarDocDTO);
$this->entityManager->persist($calendarDoc);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
);
}
return new Response(
$this->engine->render(
$view,
['calendar_doc' => $calendarDoc, 'form' => $form->createView()]
)
);
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/delete", name="chill_calendar_calendardoc_delete")
*/
public function delete(CalendarDoc $calendarDoc, Request $request): Response
{
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException('Not authorized to delete document');
}
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/delete_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/delete_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
}
$form = $this->formFactory->createBuilder()
->add('submit', SubmitType::class, [
'label' => 'Delete',
])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted()) {
$this->entityManager->remove($calendarDoc);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
);
}
return new Response(
$this->engine->render(
$view,
[
'calendar_doc' => $calendarDoc,
'form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/edit", name="chill_calendar_calendardoc_edit")
*/
public function edit(CalendarDoc $calendarDoc, Request $request): Response
{
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException();
}
// set variables
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/edit_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/edit_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
default:
throw new UnexpectedValueException('Unsupported context');
}
$calendarDocEditDTO = new CalendarDoc\CalendarDocEditDTO($calendarDoc);
$form = $this->formFactory->create(CalendarDocEditType::class, $calendarDocEditDTO);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$calendarDoc->editFromDTO($calendarDocEditDTO);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
);
}
return new Response(
$this->engine->render(
$view,
['calendar_doc' => $calendarDoc, 'form' => $form->createView()]
)
);
}
/**

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\DependencyInjection;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@ -52,9 +53,10 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
{
$this->preprendRoutes($container);
$this->prependCruds($container);
$this->prependRoleHierarchy($container);
}
protected function prependCruds(ContainerBuilder $container)
private function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
@ -130,7 +132,18 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
]);
}
protected function preprendRoutes(ContainerBuilder $container)
private function prependRoleHierarchy(ContainerBuilder $container): void
{
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
CalendarVoter::CREATE => [CalendarVoter::SEE],
CalendarVoter::EDIT => [CalendarVoter::SEE],
CalendarVoter::DELETE => [CalendarVoter::SEE],
],
]);
}
private function preprendRoutes(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'routing' => [

View File

@ -18,6 +18,7 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@ -48,7 +49,7 @@ use function in_array;
* "chill_calendar_calendar": Calendar::class
* })
*/
class Calendar implements TrackCreationInterface, TrackUpdateInterface
class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCentersInterface
{
use RemoteCalendarTrait;
@ -312,6 +313,20 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
return $this->cancelReason;
}
public function getCenters(): ?iterable
{
switch ($this->getContext()) {
case 'person':
return [$this->getPerson()->getCenter()];
case 'accompanying_period':
return $this->getAccompanyingPeriod()->getCenters();
default:
throw new LogicException('context not supported: ' . $this->getContext());
}
}
public function getComment(): CommentEmbeddable
{
return $this->comment;

View File

@ -11,6 +11,8 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocCreateDTO;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocEditDTO;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
@ -52,14 +54,14 @@ class CalendarDoc implements TrackCreationInterface, TrackUpdateInterface
* @ORM\ManyToOne(targetEntity=StoredObject::class, cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*/
private StoredObject $storedObject;
private ?StoredObject $storedObject;
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": false})
*/
private bool $trackDateTimeVersion = false;
public function __construct(Calendar $calendar, StoredObject $storedObject)
public function __construct(Calendar $calendar, ?StoredObject $storedObject)
{
$this->setCalendar($calendar);
@ -67,6 +69,22 @@ class CalendarDoc implements TrackCreationInterface, TrackUpdateInterface
$this->datetimeVersion = $calendar->getDateTimeVersion();
}
public function createFromDTO(CalendarDocCreateDTO $calendarDocCreateDTO): void
{
$this->storedObject = $calendarDocCreateDTO->doc;
$this->storedObject->setTitle($calendarDocCreateDTO->title);
}
public function editFromDTO(CalendarDocEditDTO $calendarDocEditDTO): void
{
if (null !== $calendarDocEditDTO->doc) {
$calendarDocEditDTO->doc->setTitle($this->getStoredObject()->getTitle());
$this->setStoredObject($calendarDocEditDTO->doc);
}
$this->getStoredObject()->setTitle($calendarDocEditDTO->title);
}
public function getCalendar(): Calendar
{
return $this->calendar;

View File

@ -0,0 +1,30 @@
<?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\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Validator\Constraints as Assert;
class CalendarDocCreateDTO
{
/**
* @Assert\NotNull
* @Assert\Valid
*/
public ?StoredObject $doc = null;
/**
* @Assert\NotBlank
* @Assert\NotNull
*/
public ?string $title = '';
}

View File

@ -0,0 +1,35 @@
<?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\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Validator\Constraints as Assert;
class CalendarDocEditDTO
{
/**
* @Assert\Valid
*/
public ?StoredObject $doc = null;
/**
* @Assert\NotBlank
* @Assert\NotNull
*/
public ?string $title = '';
public function __construct(CalendarDoc $calendarDoc)
{
$this->title = $calendarDoc->getStoredObject()->getTitle();
}
}

View File

@ -0,0 +1,42 @@
<?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\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocCreateDTO;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarDocCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'chill_calendar.Document title',
'required' => true,
])
->add('doc', StoredObjectType::class, [
'label' => 'chill_calendar.Document object',
'required' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CalendarDocCreateDTO::class,
]);
}
}

View File

@ -0,0 +1,41 @@
<?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\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocEditDTO;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarDocEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'chill_calendar.Document title',
'required' => true,
])
->add('doc', StoredObjectType::class, [
'label' => 'chill_calendar.Document object',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CalendarDocEditDTO::class,
]);
}
}

View File

@ -64,31 +64,38 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb;
}
public function buildQueryByAccompanyingPeriodIgnoredByDates(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb->from(Calendar::class, 'c');
$andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
$qb->setParameter('period', $period);
if (null !== $startDate) {
$andX->add($qb->expr()->lt('c.startDate', ':startDate'));
$qb->setParameter('startDate', $startDate);
}
if (null !== $endDate) {
$andX->add($qb->expr()->gt('c.endDate', ':endDate'));
$qb->setParameter('endDate', $endDate);
}
$qb->where($andX);
return $qb;
}
/**
* Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
*/
public function buildQueryByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
// find the reachable accompanying periods for person
$periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE);
$qb = $this->em->createQueryBuilder()
->from(Calendar::class, 'c');
$qb
->where(
$qb->expr()->orX(
// the calendar where the person is the main person:
$qb->expr()->eq('c.person', ':person'),
// when the calendar is in a reachable period, and contains person
$qb->expr()->andX(
$qb->expr()->in('c.accompanyingPeriod', ':periods'),
$qb->expr()->isMemberOf(':person', 'c.persons')
)
)
)
->setParameter('person', $person)
->setParameter('periods', $periods);
$this->addQueryByPersonWithoutDate($qb, $person);
// filter by date
if (null !== $startDate) {
@ -104,6 +111,30 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb;
}
/**
* Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
*/
public function buildQueryByPersonIgnoredByDates(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
$qb = $this->em->createQueryBuilder()
->from(Calendar::class, 'c');
$this->addQueryByPersonWithoutDate($qb, $person);
// filter by date
if (null !== $startDate) {
$qb->andWhere($qb->expr()->lt('c.startDate', ':startDate'))
->setParameter('startDate', $startDate);
}
if (null !== $endDate) {
$qb->andWhere($qb->expr()->gt('c.endDate', ':endDate'))
->setParameter('endDate', $endDate);
}
return $qb;
}
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)');
@ -119,6 +150,21 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
->getSingleScalarResult();
}
public function countIgnoredByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
$qb = $this->buildQueryByAccompanyingPeriodIgnoredByDates($period, $startDate, $endDate)->select('count(c)');
return $qb->getQuery()->getSingleScalarResult();
}
public function countIgnoredByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
return $this->buildQueryByPersonIgnoredByDates($person, $startDate, $endDate)
->select('COUNT(c)')
->getQuery()
->getSingleScalarResult();
}
/**
* @return array|Calendar[]
*/
@ -160,4 +206,25 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb->getQuery()->getResult();
}
private function addQueryByPersonWithoutDate(QueryBuilder $qb, Person $person): void
{
// find the reachable accompanying periods for person
$periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE);
$qb
->where(
$qb->expr()->orX(
// the calendar where the person is the main person:
$qb->expr()->eq('c.person', ':person'),
// when the calendar is in a reachable period, and contains person
$qb->expr()->andX(
$qb->expr()->in('c.accompanyingPeriod', ':periods'),
$qb->expr()->isMemberOf(':person', 'c.persons')
)
)
)
->setParameter('person', $person)
->setParameter('periods', $periods);
}
}

View File

@ -32,6 +32,18 @@ interface CalendarACLAwareRepositoryInterface
*/
public function countByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* Return the number or calendars associated with an accompanyign period which **does not** match the date conditions.
*/
public function countIgnoredByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* Return the number or calendars associated with a person which **does not** match the date conditions.
*
* See condition on @see{self::findByPerson}.
*/
public function countIgnoredByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* @return array|Calendar[]
*/

View File

@ -3,11 +3,6 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
<style lang="css">
--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;
</style>
<div class="accompanying_course_work-list">
<table class="obj-res-eval my-3">
<thead>
@ -17,27 +12,40 @@
</thead>
<tbody>
{% for d in calendar.documents %}
<tr>
<td class="eval">
<ul class="eval_title">
<li>
{{ mm.mimeIcon(d.storedObject.type) }}
{{ d.storedObject.title }}
<ul class="record_actions small inline">
{% if chill_document_is_editable(d.storedObject) %}
<li>
{{ d.storedObject|chill_document_edit_button }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_DOC_SEE', d) %}
<tr>
<td class="eval">
<ul class="eval_title">
<li>
{{ m.download_button(d.storedObject, d.storedObject.title) }}
{{ mm.mimeIcon(d.storedObject.type) }}
{{ d.storedObject.title }}
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
{% endif %}
<ul class="record_actions small inline">
{% if chill_document_is_editable(d.storedObject) and is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_delete', {'id': d.id})}}" class="btn btn-delete"></a>
</li>
<li>
{{ d.storedObject|chill_document_edit_button }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_edit', {'id': d.id})}}" class="btn btn-edit"></a>
</li>
{% endif %}
<li>
{{ m.download_button(d.storedObject, d.storedObject.title) }}
</li>
</ul>
</li>
</ul>
</li>
</ul>
</td>
</tr>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@ -112,13 +112,37 @@
<div class="item-row">
<ul class="record_actions">
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) and hasDocs %}
<li>
<a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_pick_template', {'id': calendar.id }) }}">
{{ 'chill_calendar.Add a document'|trans }}
</a>
</li>
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) %}
{% if not hasDocs %}
<li>
<a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
{{ 'chill_calendar.Add a document'|trans }}
</a>
</li>
{% else %}
<li>
<div class="dropdown">
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'chill_calendar.Add a document'|trans }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_pick_template', {'id': calendar.id }) }}">
{{ 'chill_calendar.Add a document from template'|trans }}
</a>
</li>
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
{{ 'chill_calendar.Upload a document'|trans }}
</a>
</li>
</ul>
</div>
</li>
{% endif %}
{% endif %}
{% if accompanyingCourse is defined and is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) and calendar.activity is null %}
<li>

View File

@ -27,9 +27,11 @@
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}"
class="btn btn-create button-small"></a>
{% if nbIgnored == 0 %}
{{ "There is no calendar items."|trans }}
{% else %}
{{ 'chill_calendar.There are count ignored calendars by date filter'|trans({'nbIgnored': nbIgnored}) }}
{% endif %}
</p>
{% else %}
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}

View File

@ -26,11 +26,11 @@
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'person_id': person.id}) }}"
class="btn btn-create btn-sm">
{{ 'Create'|trans }}
</a>
{% if nbIgnored == 0 %}
{{ "There is no calendar items."|trans }}
{% else %}
{{ 'chill_calendar.There are count ignored calendars by date filter'|trans({'nbIgnored': nbIgnored}) }}
{% endif %}
</p>
{% else %}
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}

View File

@ -0,0 +1,19 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Remove a calendar document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'chill_calendar.Remove a calendar document'|trans,
'confirm_question' : 'chill_calendar.Are you sure you want to remove the doc?'|trans,
'cancel_route' : 'chill_calendar_calendar_list_by_period',
'cancel_parameters' : { 'id' : accompanyingCourse.id },
'form' : form
} ) }}
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'chill_calendar.Remove a calendar document'|trans,
'confirm_question' : 'chill_calendar.Are you sure you want to remove the doc?'|trans,
'cancel_route' : 'chill_calendar_calendar_list_by_person',
'cancel_parameters' : { 'id' : person.id },
'form' : form
} ) }}
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Edit a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_accompanying_period', {'id': accompanyingCourse.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Edit a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_person', {'id': person.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Add a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_accompanying_period', {'id': accompanyingCourse.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Add a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_person', {'id': person.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,61 @@
<?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\CalendarBundle\Security\Voter;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use UnexpectedValueException;
use function in_array;
class CalendarDocVoter extends Voter
{
public const EDIT = 'CHILL_CALENDAR_DOC_EDIT';
public const SEE = 'CHILL_CALENDAR_DOC_SEE';
private const ALL = [
'CHILL_CALENDAR_DOC_EDIT',
'CHILL_CALENDAR_DOC_SEE',
];
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
protected function supports($attribute, $subject): bool
{
return in_array($attribute, self::ALL, true) && $subject instanceof CalendarDoc;
}
/**
* @param CalendarDoc $subject
* @param mixed $attribute
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
switch ($attribute) {
case self::EDIT:
return $this->security->isGranted(CalendarVoter::EDIT, $subject->getCalendar());
case self::SEE:
return $this->security->isGranted(CalendarVoter::SEE, $subject->getCalendar());
default:
throw new UnexpectedValueException('Attribute not supported: ' . $attribute);
}
}
}

View File

@ -20,13 +20,14 @@ namespace Chill\CalendarBundle\Security\Voter;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use LogicException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@ -41,6 +42,10 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
public const SEE = 'CHILL_CALENDAR_CALENDAR_SEE';
private AuthorizationHelperInterface $authorizationHelper;
private CenterResolverManagerInterface $centerResolverManager;
private Security $security;
private VoterHelperInterface $voterHelper;
@ -52,8 +57,8 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
$this->security = $security;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(AccompanyingPeriod::class, [self::SEE])
->addCheckFor(Person::class, [self::SEE])
->addCheckFor(AccompanyingPeriod::class, [self::SEE, self::CREATE])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Calendar::class, [self::SEE, self::CREATE, self::EDIT, self::DELETE])
->build();
}
@ -93,48 +98,37 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
if ($subject instanceof AccompanyingPeriod) {
switch ($attribute) {
case self::SEE:
case self::CREATE:
if ($subject->getStep() === AccompanyingPeriod::STEP_DRAFT) {
return false;
}
// we first check here that the user has read access to the period
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject);
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject)) {
return false;
}
default:
throw new LogicException('subject not implemented');
// There is no scope on Calendar, but there are some on accompanying period
// so, to ignore AccompanyingPeriod's scopes, we create a blank Calendar
// linked with an accompanying period.
return $this->voterHelper->voteOnAttribute($attribute, (new Calendar())->setAccompanyingPeriod($subject), $token);
}
} elseif ($subject instanceof Person) {
switch ($attribute) {
case self::SEE:
return $this->security->isGranted(PersonVoter::SEE, $subject);
default:
throw new LogicException('subject not implemented');
case self::CREATE:
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
} elseif ($subject instanceof Calendar) {
if (null !== $period = $subject->getAccompanyingPeriod()) {
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $period);
case self::DELETE:
return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period);
}
} elseif (null !== $person = $subject->getPerson()) {
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
return $this->security->isGranted(PersonVoter::SEE, $person);
case self::DELETE:
return $this->security->isGranted(PersonVoter::UPDATE, $person);
}
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
case self::DELETE:
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
}
throw new LogicException('attribute not implemented');
throw new LogicException('attribute or not implemented');
}
}

View File

@ -0,0 +1,63 @@
<?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\CalendarBundle\Tests\Entity;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class CalendarDocTest extends TestCase
{
public function testCreateEditFromDTO(): void
{
$doc = new CalendarDoc(new Calendar(), null);
$create = new CalendarDoc\CalendarDocCreateDTO();
$create->title = 'tagada';
$create->doc = $obj1 = new StoredObject();
$doc->createFromDTO($create);
$this->assertSame($obj1, $doc->getStoredObject());
$this->assertEquals('tagada', $doc->getStoredObject()->getTitle());
$edit = new CalendarDoc\CalendarDocEditDTO($doc);
$edit->title = 'tsointsoin';
$doc->editFromDTO($edit);
$this->assertSame($obj1, $doc->getStoredObject());
$this->assertEquals('tsointsoin', $doc->getStoredObject()->getTitle());
$edit2 = new CalendarDoc\CalendarDocEditDTO($doc);
$edit2->doc = $obj2 = new StoredObject();
$doc->editFromDTO($edit2);
$this->assertSame($obj2, $doc->getStoredObject());
$this->assertEquals('tsointsoin', $doc->getStoredObject()->getTitle());
$edit3 = new CalendarDoc\CalendarDocEditDTO($doc);
$edit3->doc = $obj3 = new StoredObject();
$edit3->title = 'tagada';
$doc->editFromDTO($edit3);
$this->assertSame($obj3, $doc->getStoredObject());
$this->assertEquals('tagada', $doc->getStoredObject()->getTitle());
}
}

View File

@ -0,0 +1,8 @@
chill_calendar:
There are count ignored calendars by date filter: >-
{nbIgnored, plural,
=0 {Il n'y a aucun rendez-vous ignoré par le filtre de date.}
one {Il y a un rendez-vous ignoré par le filtre de date. Modifiez le filtre de date pour le voir apparaitre.}
few {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
other {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
}

View File

@ -55,6 +55,14 @@ chill_calendar:
Create and add a document: Créer et ajouter un document
Save and add a document: Enregistrer et ajouter un document
Create for me: Créer un rendez-vous pour moi-même
Edit a document: Modifier un document
Document title: Titre
Document object: Document
Add a document from template: Ajouter un document depuis un gabarit
Upload a document: Téléverser un document
Remove a calendar document: Supprimer un document d'un rendez-vous
Are you sure you want to remove the doc?: Êtes-vous sûr·e de vouloir supprimer le document associé ?
Document outdated: La date et l'heure du rendez-vous ont été modifiés après la création du document
remote_ms_graph:

View File

@ -309,7 +309,7 @@ class AuthorizationHelper implements AuthorizationHelperInterface
if ($this->isRoleReached($attribute, $roleScope->getRole())) {
//if yes, we have a right on something...
// perform check on scope if necessary
if ($this->scopeResolverDispatcher->isConcerned($entity)) {
if ($this->scopeResolverDispatcher->isConcerned($entity)) {// here, we should also check that the role need a scope
$scope = $this->scopeResolverDispatcher->resolveScope($entity);
if (null === $scope) {

View File

@ -53,7 +53,7 @@ use Symfony\Component\Validator\GroupSequenceProviderInterface;
use UnexpectedValueException;
use function in_array;
use function array_key_exists;
use const SORT_REGULAR;
/**
@ -644,16 +644,18 @@ class AccompanyingPeriod implements
public function getCenters(): ?iterable
{
$centers = [];
foreach ($this->getPersons() as $person) {
if (
!in_array($person->getCenter(), $centers ?? [], true)
&& null !== $person->getCenter()
null !== $person->getCenter()
&& !array_key_exists(spl_object_hash($person->getCenter()), $centers)
) {
$centers[] = $person->getCenter();
$centers[spl_object_hash($person->getCenter())] = $person->getCenter();
}
}
return $centers ?? null;
return array_values($centers);
}
/**