diff --git a/.changes/unreleased/Feature-20240208-210138.yaml b/.changes/unreleased/Feature-20240208-210138.yaml new file mode 100644 index 000000000..977cd76cd --- /dev/null +++ b/.changes/unreleased/Feature-20240208-210138.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Modernize the event bundle, with some new fields and multiple improvements +time: 2024-02-08T21:01:38.124701724+01:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillEventBundle/Controller/EventController.php b/src/Bundle/ChillEventBundle/Controller/EventController.php index e383c5432..7cbd2fbbe 100644 --- a/src/Bundle/ChillEventBundle/Controller/EventController.php +++ b/src/Bundle/ChillEventBundle/Controller/EventController.php @@ -17,10 +17,11 @@ use Chill\EventBundle\Form\EventType; use Chill\EventBundle\Form\Type\PickEventType; use Chill\EventBundle\Security\Authorization\EventVoter; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Pagination\PaginatorFactory; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\PersonBundle\Entity\Person; -use Chill\PersonBundle\Form\Type\PickPersonType; +use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Privacy\PrivacyEvent; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Csv; @@ -37,53 +38,26 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; /** * Class EventController. */ -class EventController extends AbstractController +final class EventController extends AbstractController { - /** - * @var AuthorizationHelper - */ - protected $authorizationHelper; - - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - - /** - * @var FormFactoryInterface - */ - protected $formFactoryInterface; - - /** - * @var PaginatorFactory - */ - protected $paginator; - - /** - * @var TranslatorInterface - */ - protected $translator; - /** * EventController constructor. */ public function __construct( - EventDispatcherInterface $eventDispatcher, - AuthorizationHelper $authorizationHelper, - FormFactoryInterface $formFactoryInterface, - TranslatorInterface $translator, - PaginatorFactory $paginator + private readonly EventDispatcherInterface $eventDispatcher, + private readonly AuthorizationHelperInterface $authorizationHelper, + private readonly FormFactoryInterface $formFactoryInterface, + private readonly TranslatorInterface $translator, + private readonly PaginatorFactory $paginator, + private readonly Security $security, ) { - $this->eventDispatcher = $eventDispatcher; - $this->authorizationHelper = $authorizationHelper; - $this->formFactoryInterface = $formFactoryInterface; - $this->translator = $translator; - $this->paginator = $paginator; } /** @@ -181,7 +155,7 @@ class EventController extends AbstractController $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); - $reachablesCircles = $this->authorizationHelper->getReachableCircles( + $reachablesCircles = $this->authorizationHelper->getReachableScopes( $this->getUser(), EventVoter::SEE, $person->getCenter() @@ -233,6 +207,12 @@ class EventController extends AbstractController */ public function newAction(?Center $center, Request $request) { + $user = $this->security->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException('not a regular user. Maybe an administrator ?'); + } + if (null === $center) { $center_id = $request->query->get('center_id'); $center = $this->getDoctrine()->getRepository(Center::class)->find($center_id); @@ -240,6 +220,7 @@ class EventController extends AbstractController $entity = new Event(); $entity->setCenter($center); + $entity->setLocation($user->getCurrentLocation()); $form = $this->createCreateForm($entity); $form->handleRequest($request); @@ -282,7 +263,7 @@ class EventController extends AbstractController } $form = $this->formFactoryInterface - ->createNamedBuilder(null, FormType::class, null, [ + ->createNamedBuilder('', FormType::class, null, [ 'csrf_protection' => false, ]) ->setMethod('GET') @@ -323,7 +304,7 @@ class EventController extends AbstractController } $this->denyAccessUnlessGranted( - 'CHILL_EVENT_SEE_DETAILS', + EventVoter::SEE_DETAILS, $event, 'You are not allowed to see details on this event' ); @@ -367,7 +348,7 @@ class EventController extends AbstractController $this->addFlash('success', $this->translator ->trans('The event was updated')); - return $this->redirectToRoute('chill_event__event_edit', ['event_id' => $event_id]); + return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]); } return $this->render('@ChillEvent/Event/edit.html.twig', [ @@ -385,7 +366,7 @@ class EventController extends AbstractController { /** @var \Symfony\Component\Form\FormBuilderInterface $builder */ $builder = $this - ->get('form.factory') + ->formFactoryInterface ->createNamedBuilder( null, FormType::class, @@ -430,11 +411,9 @@ class EventController extends AbstractController */ protected function createAddParticipationByPersonForm(Event $event) { - /** @var \Symfony\Component\Form\FormBuilderInterface $builder */ - $builder = $this - ->get('form.factory') + $builder = $this->formFactoryInterface ->createNamedBuilder( - null, + '', FormType::class, null, [ @@ -444,23 +423,17 @@ class EventController extends AbstractController ] ); - $builder->add('person_id', PickPersonType::class, [ - 'role' => 'CHILL_EVENT_CREATE', - 'centers' => $event->getCenter(), + $builder->add('person_id', PickPersonDynamicType::class, [ + 'as_id' => true, + 'multiple' => false, + 'submit_on_adding_new_entity' => true, + 'label' => 'Add a participation', ]); $builder->add('event_id', HiddenType::class, [ 'data' => $event->getId(), ]); - $builder->add( - 'submit', - SubmitType::class, - [ - 'label' => 'Add a participation', - ] - ); - return $builder->getForm(); } @@ -469,7 +442,7 @@ class EventController extends AbstractController */ protected function createExportByFormatForm() { - $builder = $this->createFormBuilder() + $builder = $this->createFormBuilder(['format' => 'xlsx']) ->add('format', ChoiceType::class, [ 'choices' => [ 'xlsx' => 'xlsx', diff --git a/src/Bundle/ChillEventBundle/Controller/EventListController.php b/src/Bundle/ChillEventBundle/Controller/EventListController.php new file mode 100644 index 000000000..8af005875 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Controller/EventListController.php @@ -0,0 +1,118 @@ +buildFilterOrder(); + $filterData = [ + 'q' => (string) $filter->getQueryString(), + 'dates' => $filter->getDateRangeData('dates'), + 'event_types' => $filter->getEntityChoiceData('event_types'), + ]; + $total = $this->eventACLAwareRepository->countAllViewable($filterData); + $pagination = $this->paginatorFactory->create($total); + $events = $this->eventACLAwareRepository->findAllViewable($filterData, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage()); + $eventForms = []; + foreach ($events as $event) { + $eventForms[$event->getId()] = $this->createAddParticipationByPersonForm($event)->createView(); + } + + return new Response($this->environment->render( + '@ChillEvent/Event/page_list.html.twig', + [ + 'events' => $events, + 'pagination' => $pagination, + 'eventForms' => $eventForms, + 'filter' => $filter, + ] + )); + } + + private function buildFilterOrder(): FilterOrderHelper + { + $types = $this->eventTypeRepository->findAllActive(); + + $builder = $this->filterOrderHelperFactory->create(__METHOD__); + $builder + ->addDateRange('dates', 'event.filter.event_dates') + ->addSearchBox(['name']) + ->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [ + 'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()), + ]); + + return $builder->build(); + } + + private function createAddParticipationByPersonForm(Event $event): FormInterface + { + $builder = $this->formFactory + ->createNamedBuilder( + '', + FormType::class, + null, + [ + 'method' => 'GET', + 'action' => $this->urlGenerator->generate('chill_event_participation_new'), + 'csrf_protection' => false, + ] + ); + + $builder->add('person_id', PickPersonDynamicType::class, [ + 'as_id' => true, + 'multiple' => false, + 'submit_on_adding_new_entity' => true, + 'label' => 'Add a participation', + ]); + + $builder->add('event_id', HiddenType::class, [ + 'data' => $event->getId(), + ]); + + return $builder->getForm(); + } +} diff --git a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php index 9785c20b0..06a8dda29 100644 --- a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php +++ b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php @@ -14,7 +14,10 @@ namespace Chill\EventBundle\Controller; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Form\ParticipationType; +use Chill\EventBundle\Repository\EventRepository; use Chill\EventBundle\Security\Authorization\ParticipationVoter; +use Chill\PersonBundle\Repository\PersonRepository; +use Chill\PersonBundle\Security\Authorization\PersonVoter; use Doctrine\Common\Collections\Collection; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -28,13 +31,17 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * Class ParticipationController. */ -class ParticipationController extends AbstractController +final class ParticipationController extends AbstractController { /** * ParticipationController constructor. */ - public function __construct(private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) - { + public function __construct( + private readonly LoggerInterface $logger, + private readonly TranslatorInterface $translator, + private readonly EventRepository $eventRepository, + private readonly PersonRepository $personRepository, + ) { } /** @@ -230,6 +237,7 @@ class ParticipationController extends AbstractController return $this->render('@ChillEvent/Participation/new.html.twig', [ 'form' => $form->createView(), 'participation' => $participation, + 'ignored_participations' => [], ]); } @@ -539,7 +547,7 @@ class ParticipationController extends AbstractController * If the request is multiple, the $participation object is cloned. * Limitations: the $participation should not be persisted. * - * @return Participation|Participation[] return one single participation if $multiple == false + * @return Participation|list return one single participation if $multiple == false * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the event/person is not found * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException if the user does not have access to event/person @@ -556,30 +564,25 @@ class ParticipationController extends AbstractController } $event_id = $request->query->getInt('event_id', 0); // sf4 check: - // prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given` - if (null !== $event_id) { - $event = $em->getRepository(Event::class) - ->find($event_id); + $event = $this->eventRepository->find($event_id); - if (null === $event) { - throw $this->createNotFoundException('The event with id '.$event_id.' is not found'); - } - - $this->denyAccessUnlessGranted( - 'CHILL_EVENT_SEE', - $event, - 'The user is not allowed to see the event' - ); - - $participation->setEvent($event); + if (null === $event) { + throw $this->createNotFoundException('The event with id '.$event_id.' is not found'); } + $this->denyAccessUnlessGranted( + 'CHILL_EVENT_SEE', + $event, + 'The user is not allowed to see the event' + ); + + $participation->setEvent($event); + // this script should be able to handle multiple, so we translate // single person_id in an array $persons_ids = $request->query->has('person_id') ? - [$request->query->getInt('person_id', 0)] // sf4 check: - // prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given` + [$request->query->get('person_id', 0)] : explode(',', (string) $request->query->get('persons_ids')); $participations = []; @@ -588,15 +591,14 @@ class ParticipationController extends AbstractController $participation = \count($persons_ids) > 1 ? clone $participation : $participation; if (null !== $person_id) { - $person = $em->getRepository(\Chill\PersonBundle\Entity\Person::class) - ->find($person_id); + $person = $this->personRepository->find($person_id); if (null === $person) { throw $this->createNotFoundException('The person with id '.$person_id.' is not found'); } $this->denyAccessUnlessGranted( - 'CHILL_PERSON_SEE', + PersonVoter::SEE, $person, 'The user is not allowed to see the person' ); diff --git a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php index 07c431f2f..8ddcab58c 100644 --- a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php +++ b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\EventBundle\DependencyInjection; use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\Authorization\ParticipationVoter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -33,10 +34,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/authorization.yaml'); - $loader->load('services/controller.yaml'); $loader->load('services/fixtures.yaml'); $loader->load('services/forms.yaml'); - $loader->load('services/menu.yaml'); $loader->load('services/repositories.yaml'); $loader->load('services/search.yaml'); $loader->load('services/timeline.yaml'); @@ -61,6 +60,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface EventVoter::SEE_DETAILS => [EventVoter::SEE], EventVoter::UPDATE => [EventVoter::SEE_DETAILS], EventVoter::CREATE => [EventVoter::SEE_DETAILS], + ParticipationVoter::SEE_DETAILS => [ParticipationVoter::SEE], + ParticipationVoter::UPDATE => [ParticipationVoter::SEE_DETAILS], ], ]); } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index b5d013588..44f716edf 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -11,15 +11,23 @@ declare(strict_types=1); namespace Chill\EventBundle\Entity; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; +use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; +use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\HasScopeInterface; +use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; /** * Class Event. @@ -30,10 +38,15 @@ use Doctrine\ORM\Mapping as ORM; * * @ORM\HasLifecycleCallbacks */ -class Event implements HasCenterInterface, HasScopeInterface +class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface { + use TrackCreationTrait; + use TrackUpdateTrait; + /** - * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center") + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A + * + * @Assert\NotNull() */ private ?Center $center = null; @@ -63,6 +76,8 @@ class Event implements HasCenterInterface, HasScopeInterface /** * @ORM\Column(type="string", length=150) + * + * @Assert\NotBlank() */ private ?string $name = null; @@ -77,15 +92,45 @@ class Event implements HasCenterInterface, HasScopeInterface /** * @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\EventType") + * + * @Assert\NotNull() */ private ?EventType $type = null; + /** + * @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_") + */ + private CommentEmbeddable $comment; + + /** + * @ORM\ManyToOne(targetEntity=Location::class) + * + * @ORM\JoinColumn(nullable=true) + */ + private ?Location $location = null; + + /** + * @var Collection + * + * @ORM\ManyToMany(targetEntity=StoredObject::class, cascade={"persist","refresh"}) + * + * @ORM\JoinTable("chill_event_event_documents") + */ + private Collection $documents; + + /** + * @ORM\Column(type="decimal", precision=10, scale=4, nullable=true, options={"default": null}) + */ + private string $organizationCost = '0.0'; + /** * Event constructor. */ public function __construct() { $this->participations = new ArrayCollection(); + $this->documents = new ArrayCollection(); + $this->comment = new CommentEmbeddable(); } /** @@ -100,6 +145,22 @@ class Event implements HasCenterInterface, HasScopeInterface return $this; } + public function addDocument(StoredObject $storedObject): self + { + if ($this->documents->contains($storedObject)) { + $this->documents[] = $storedObject; + } + + return $this; + } + + public function removeDocument(StoredObject $storedObject): self + { + $this->documents->removeElement($storedObject); + + return $this; + } + /** * @return Center */ @@ -259,4 +320,44 @@ class Event implements HasCenterInterface, HasScopeInterface return $this; } + + public function getComment(): CommentEmbeddable + { + return $this->comment; + } + + public function setComment(CommentEmbeddable $comment): void + { + $this->comment = $comment; + } + + public function getLocation(): ?Location + { + return $this->location; + } + + public function setLocation(?Location $location): void + { + $this->location = $location; + } + + public function getDocuments(): Collection + { + return $this->documents; + } + + public function setDocuments(Collection $documents): void + { + $this->documents = $documents; + } + + public function getOrganizationCost(): string + { + return $this->organizationCost; + } + + public function setOrganizationCost(string $organizationCost): void + { + $this->organizationCost = $organizationCost; + } } diff --git a/src/Bundle/ChillEventBundle/Entity/Participation.php b/src/Bundle/ChillEventBundle/Entity/Participation.php index 464feaf30..ded8ae8da 100644 --- a/src/Bundle/ChillEventBundle/Entity/Participation.php +++ b/src/Bundle/ChillEventBundle/Entity/Participation.php @@ -11,13 +11,17 @@ declare(strict_types=1); namespace Chill\EventBundle\Entity; -use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; +use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; +use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\HasScopeInterface; use Chill\MainBundle\Entity\Scope; use Chill\PersonBundle\Entity\Person; -use DateTime; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** @@ -26,12 +30,20 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * @ORM\Entity( * repositoryClass="Chill\EventBundle\Repository\ParticipationRepository") * - * @ORM\Table(name="chill_event_participation") + * @ORM\Table(name="chill_event_participation", uniqueConstraints={ + * + * @ORM\UniqueConstraint(name="chill_event_participation_event_person_unique_idx", columns={"event_id", "person_id"}) + * }) * * @ORM\HasLifecycleCallbacks + * + * @UniqueEntity({"event", "person"}, message="event.validation.person_already_participate_to_event") */ -class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface +class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface, TrackUpdateInterface, TrackCreationInterface { + use TrackCreationTrait; + use TrackUpdateTrait; + /** * @ORM\ManyToOne( * targetEntity="Chill\EventBundle\Entity\Event", @@ -48,13 +60,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa */ private ?int $id = null; - /** - * @ORM\Column(type="datetime") - */ - private ?\DateTime $lastUpdate = null; - /** * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person") + * + * @Assert\NotNull() */ private ?Person $person = null; @@ -65,12 +74,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status") + * + * @Assert\NotNull() */ private ?Status $status = null; - /** - * @return Center - */ public function getCenter() { if (null === $this->getEvent()) { @@ -90,10 +98,8 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get id. - * - * @return int */ - public function getId() + public function getId(): int { return $this->id; } @@ -101,11 +107,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get lastUpdate. * - * @return \DateTime + * @return \DateTimeInterface|null */ public function getLastUpdate() { - return $this->lastUpdate; + return $this->getUpdatedAt(); } /** @@ -235,10 +241,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa */ public function setEvent(?Event $event = null) { - if ($this->event !== $event) { - $this->update(); - } - $this->event = $event; return $this; @@ -251,10 +253,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa */ public function setPerson(?Person $person = null) { - if ($person !== $this->person) { - $this->update(); - } - $this->person = $person; return $this; @@ -267,9 +265,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa */ public function setRole(?Role $role = null) { - if ($role !== $this->role) { - $this->update(); - } $this->role = $role; return $this; @@ -282,10 +277,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa */ public function setStatus(?Status $status = null) { - if ($this->status !== $status) { - $this->update(); - } - $this->status = $status; return $this; @@ -295,11 +286,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa * Set lastUpdate. * * @return Participation + * + * @deprecated */ protected function update() { - $this->lastUpdate = new \DateTime('now'); - return $this; } } diff --git a/src/Bundle/ChillEventBundle/Form/EventType.php b/src/Bundle/ChillEventBundle/Form/EventType.php index a06999d2b..fe64ef12b 100644 --- a/src/Bundle/ChillEventBundle/Form/EventType.php +++ b/src/Bundle/ChillEventBundle/Form/EventType.php @@ -11,12 +11,18 @@ declare(strict_types=1); namespace Chill\EventBundle\Form; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Form\StoredObjectType; use Chill\EventBundle\Form\Type\PickEventTypeType; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillDateTimeType; +use Chill\MainBundle\Form\Type\CommentType; +use Chill\MainBundle\Form\Type\PickUserLocationType; use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Form\Type\UserPickerType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -47,6 +53,28 @@ class EventType extends AbstractType 'class' => '', ], 'required' => false, + ]) + ->add('location', PickUserLocationType::class, [ + 'label' => 'event.fields.location', + ]) + ->add('comment', CommentType::class, [ + 'label' => 'Comment', + 'required' => false, + ]) + ->add('documents', ChillCollectionType::class, [ + 'entry_type' => StoredObjectType::class, + 'entry_options' => [ + 'has_title' => true, + ], + 'allow_add' => true, + 'allow_delete' => true, + 'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(), + 'button_remove_label' => 'event.form.remove_document', + 'button_add_label' => 'event.form.add_document', + ]) + ->add('organizationCost', MoneyType::class, [ + 'label' => 'event.fields.organizationCost', + 'help' => 'event.form.organisationCost_help', ]); } diff --git a/src/Bundle/ChillEventBundle/Form/Type/PickEventType.php b/src/Bundle/ChillEventBundle/Form/Type/PickEventType.php index 487c0c3b2..f3c712801 100644 --- a/src/Bundle/ChillEventBundle/Form/Type/PickEventType.php +++ b/src/Bundle/ChillEventBundle/Form/Type/PickEventType.php @@ -114,7 +114,7 @@ final class PickEventType extends AbstractType } else { $centers = $this->authorizationHelper->getReachableCenters( $user, - (string) $options['role']->getRole() + $options['role'] ); } diff --git a/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php b/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php new file mode 100644 index 000000000..2c78392ee --- /dev/null +++ b/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php @@ -0,0 +1,46 @@ +security->isGranted(EventVoter::SEE)) { + $menu->addChild( + $this->translator->trans('Events'), + [ + 'route' => 'chill_event_event_list', + ] + )->setExtras([ + 'order' => 250, + ]); + } + } + + public static function getMenuIds(): array + { + return ['section']; + } +} diff --git a/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php new file mode 100644 index 000000000..f92b1e825 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php @@ -0,0 +1,142 @@ +security->getUser() instanceof User) { + return 0; + } + + $qb = $this->buildQueryByAllViewable($filters); + $this->addFilters($filters, $qb); + + $qb->select('COUNT(event.id)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + + public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array + { + if (!$this->security->getUser() instanceof User) { + return []; + } + + $qb = $this->buildQueryByAllViewable($filters)->select('event'); + $this->addFilters($filters, $qb); + + $qb->setFirstResult($offset)->setMaxResults($limit); + + $qb->addOrderBy('event.date', 'DESC'); + + return $qb->getQuery()->getResult(); + } + + private function addFilters(array $filters, QueryBuilder $qb): void + { + if (($filters['q'] ?? '') !== '') { + $qb->andWhere('event.name LIKE :content'); + $qb->setParameter('content', '%'.$filters['q'].'%'); + } + + if (array_key_exists('dates', $filters)) { + $dates = $filters['dates']; + if (null !== ($dates['from'] ?? null)) { + $qb->andWhere('event.date >= :date_from'); + $qb->setParameter('date_from', $dates['from']); + } + if (null !== ($dates['to'] ?? null)) { + $qb->andWhere('event.date <= :date_to'); + $qb->setParameter('date_to', $dates['to']); + } + } + + if (0 < count($filters['event_types'] ?? [])) { + $qb->andWhere('event.type IN (:event_types)'); + $qb->setParameter('event_types', $filters['event_types']); + } + } + + public function buildQueryByAllViewable(array $filters): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder(); + $qb->from(Event::class, 'event'); + + $aclConditions = $qb->expr()->orX(); + + $i = 0; + foreach ($this->authorizationHelperForCurrentUser->getReachableCenters(EventVoter::SEE) as $center) { + foreach ($this->authorizationHelperForCurrentUser->getReachableScopes(EventVoter::SEE, $center) as $scopes) { + $aclConditions->add( + $qb->expr()->andX( + 'event.circle IN (:scopes_'.$i.')', + $qb->expr()->orX( + 'event.center = :center_'.$i, + $qb->expr()->exists( + 'SELECT 1 FROM '.Participation::class.' participation_'.$i.' JOIN participation_'.$i.'.event event_'.$i. + ' JOIN '.Person\PersonCenterHistory::class.' person_center_history_'.$i. + ' WITH IDENTITY(person_center_history_'.$i.'.person) = IDENTITY(participation_'.$i.'.person) '. + ' AND event_'.$i.'.date <= person_center_history_'.$i.'.startDate AND (person_center_history_'.$i.'.endDate IS NULL OR person_center_history_'.$i.'.endDate > event_'.$i.'.date) '. + ' WHERE participation_'.$i.'.event = event' + ) + ) + ) + ); + $qb->setParameter('scopes_'.$i, $scopes); + $qb->setParameter('center_'.$i, $center); + + ++$i; + } + } + + if (0 === $i) { + $aclConditions->add('FALSE = TRUE'); + } + + $qb + ->andWhere( + $qb->expr()->orX( + 'event.createdBy = :user', + $aclConditions + ) + ); + + $qb->setParameter('user', $this->security->getUser()); + + return $qb; + } +} diff --git a/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepositoryInterface.php b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepositoryInterface.php new file mode 100644 index 000000000..3c1da7c0a --- /dev/null +++ b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepositoryInterface.php @@ -0,0 +1,30 @@ +} $filters + */ + public function countAllViewable(array $filters): int; + + /** + * @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list} $filters + * + * @return list + */ + public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array; +} diff --git a/src/Bundle/ChillEventBundle/Repository/EventTypeRepository.php b/src/Bundle/ChillEventBundle/Repository/EventTypeRepository.php new file mode 100644 index 000000000..28e5672ea --- /dev/null +++ b/src/Bundle/ChillEventBundle/Repository/EventTypeRepository.php @@ -0,0 +1,44 @@ + + */ +final class EventTypeRepository extends ServiceEntityRepository +{ + public function __construct( + ManagerRegistry $registry, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator + ) { + parent::__construct($registry, EventType::class); + } + + /** + * @return list + */ + public function findAllActive(): array + { + $dql = 'SELECT et FROM '.EventType::class.' et WHERE et.active = TRUE ORDER BY JSON_EXTRACT(et.name, :lang)'; + + return $this->entityManager->createQuery($dql) + ->setParameter('lang', $this->translator->getLocale()) + ->getResult(); + } +} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig index a528f1f5c..fc4001c63 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig @@ -14,15 +14,16 @@ {{ form_row(edit_form.type, { 'label': 'Event type' }) }} {{ form_row(edit_form.moderator) }} + {{ form_row(edit_form.location) }} + {{ form_row(edit_form.organizationCost) }} -