diff --git a/.gitignore b/.gitignore index 9f103fd7c..a06b406d1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,8 @@ app/config/parameters.ini app/config/parameters.yml Tests/Fixtures/App/config/parameters.yml +# fixtures +Resources/test/* + #composer composer.lock diff --git a/Controller/EventController.php b/Controller/EventController.php index ca0460c14..ddc6e4ebc 100644 --- a/Controller/EventController.php +++ b/Controller/EventController.php @@ -4,8 +4,13 @@ namespace Chill\EventBundle\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Chill\PersonBundle\Form\Type\PickPersonType; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Form\EventType; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; /** @@ -103,11 +108,52 @@ class EventController extends Controller $this->denyAccessUnlessGranted('CHILL_EVENT_SEE_DETAILS', $entity, "You are not allowed to see details on this event"); + + $addParticipationByPersonForm = $this->createAddParticipationByPersonForm($entity); return $this->render('ChillEventBundle:Event:show.html.twig', array( - 'event' => $entity + 'event' => $entity, + 'form_add_participation_by_person' => $addParticipationByPersonForm->createView() )); } + + /** + * create a form to add a participation with a person + * + * @return \Symfony\Component\Form\FormInterface + */ + protected function createAddParticipationByPersonForm(Event $event) + { + /* @var $builder \Symfony\Component\Form\FormBuilderInterface */ + $builder = $this + ->get('form.factory') + ->createNamedBuilder( + null, + FormType::class, + null, + array( + 'method' => 'GET', + 'action' => $this->generateUrl('chill_event_participation_new'), + 'csrf_protection' => false + )) + ; + + $builder->add('person_id', PickPersonType::class, array( + 'role' => new Role('CHILL_EVENT_CREATE'), + 'centers' => $event->getCenter() + )); + + $builder->add('event_id', HiddenType::class, array( + 'data' => $event->getId() + )); + + $builder->add('submit', SubmitType::class, + array( + 'label' => 'Add a participation' + )); + + return $builder->getForm(); + } /** * Displays a form to edit an existing Event entity. diff --git a/Controller/ParticipationController.php b/Controller/ParticipationController.php new file mode 100644 index 000000000..8b9685462 --- /dev/null +++ b/Controller/ParticipationController.php @@ -0,0 +1,151 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\EventBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Request; +use Chill\EventBundle\Entity\Participation; +use Chill\EventBundle\Form\ParticipationType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Chill\EventBundle\Security\Authorization\ParticipationVoter; + +/** + * + * + * @author Julien Fastré + */ +class ParticipationController extends Controller +{ + public function newAction(Request $request) + { + $participation = $this->handleRequest($request, new Participation()); + + $this->denyAccessUnlessGranted(ParticipationVoter::CREATE, + $participation, 'The user is not allowed to create this participation'); + + $form = $this->createCreateForm($participation); + + return $this->render('ChillEventBundle:Participation:new.html.twig', array( + 'form' => $form->createView(), + 'participation' => $participation + )); + } + + public function createAction(Request $request) + { + $participation = $this->handleRequest($request, new Participation()); + + $this->denyAccessUnlessGranted(ParticipationVoter::CREATE, + $participation, 'The user is not allowed to create this participation'); + + $form = $this->createCreateForm($participation); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + + $em->persist($participation); + $em->flush(); + + $this->addFlash('success', $this->get('translator')->trans( + 'The participation was created' + )); + + return $this->redirectToRoute('event_show', array( + 'event_id' => $participation->getEvent()->getId() + )); + } + + return $this->render('ChillEventBundle:Participation:new.html.twig', array( + 'form' => $form->createView(), + 'participation' => $participation + )); + } + + /** + * + * @param Request $request + * @param Participation $participation + * @return Participation + * @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 + */ + protected function handleRequest(Request $request, Participation $participation) + { + $em = $this->getDoctrine()->getManager(); + + $event_id = $request->query->getInt('event_id', null); + + if ($event_id !== NULL) { + $event = $em->getRepository('ChillEventBundle:Event') + ->find($event_id); + + if ($event === NULL) { + 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); + } + + $person_id = $request->query->getInt('person_id', null); + + if ($person_id !== NULL) { + $person = $em->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException('The person with id '.$person_id.' is not found'); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person, + 'The user is not allowed to see the person'); + + $participation->setPerson($person); + } + + return $participation; + } + + /** + * + * @param Participation $participation + * @return \Symfony\Component\Form\FormInterface + */ + public function createCreateForm(Participation $participation) + { + $form = $this->createForm(ParticipationType::class, $participation, array( + 'event_type' => $participation->getEvent()->getType(), + 'action' => $this->generateUrl('chill_event_participation_create', array( + 'event_id' => $participation->getEvent()->getId(), + 'person_id' => $participation->getPerson()->getId() + )) + )); + + $form->add('submit', SubmitType::class, array( + 'label' => 'Create' + )); + + return $form; + } + +} diff --git a/DataFixtures/ORM/LoadRolesACL.php b/DataFixtures/ORM/LoadRolesACL.php index c0236d25c..08c0a8feb 100644 --- a/DataFixtures/ORM/LoadRolesACL.php +++ b/DataFixtures/ORM/LoadRolesACL.php @@ -56,19 +56,31 @@ class LoadRolesACL extends AbstractFixture implements OrderedFixtureInterface break; } - printf("Adding CHILL_EVENT_UPDATE & CHILL_EVENT_CREATE to %s " + printf("Adding CHILL_EVENT_UPDATE & CHILL_EVENT_CREATE " + . "CHILL_EVENT_PARTICIPATION_UPDATE & CHILL_EVENT_PARTICIPATION_CREATE " + . "to %s " . "permission group, scope '%s' \n", $permissionsGroup->getName(), $scope->getName()['en']); $roleScopeUpdate = (new RoleScope()) ->setRole('CHILL_EVENT_UPDATE') ->setScope($scope); + $roleScopeUpdate2 = (new RoleScope()) + ->setRole('CHILL_EVENT_PARTICIPATION_UPDATE') + ->setScope($scope); $permissionsGroup->addRoleScope($roleScopeUpdate); + $permissionsGroup->addRoleScope($roleScopeUpdate2); $roleScopeCreate = (new RoleScope()) ->setRole('CHILL_EVENT_CREATE') ->setScope($scope); + $roleScopeCreate2 = (new RoleScope()) + ->setRole('CHILL_EVENT_PARTICIPATION_CREATE') + ->setScope($scope); $permissionsGroup->addRoleScope($roleScopeCreate); + $permissionsGroup->addRoleScope($roleScopeCreate2); $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeUpdate2); $manager->persist($roleScopeCreate); + $manager->persist($roleScopeCreate2); } } diff --git a/Entity/Participation.php b/Entity/Participation.php index cc8b8544e..ef33c6b99 100644 --- a/Entity/Participation.php +++ b/Entity/Participation.php @@ -2,10 +2,14 @@ namespace Chill\EventBundle\Entity; +use Chill\MainBundle\Entity\HasScopeInterface; +use Chill\MainBundle\Entity\HasCenterInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + /** * Participation */ -class Participation +class Participation implements HasCenterInterface, HasScopeInterface { /** * @var integer @@ -183,4 +187,50 @@ class Participation { return $this->status; } + + public function getCenter() + { + if ($this->getEvent() === NULL) { + throw new \RuntimeException('The event is not linked with this instance. ' + . 'You should initialize the event with a valid center before.'); + } + + return $this->getEvent()->getCenter(); + } + + public function getScope() + { + if ($this->getEvent() === NULL) { + throw new \RuntimeException('The event is not linked with this instance. ' + . 'You should initialize the event with a valid center before.'); + } + + return $this->getEvent()->getCircle(); + } + + /** + * Check that : + * + * - the role can be associated with this event type + * - the status can be associated with this event type + * + * @param ExecutionContextInterface $context + */ + public function isConsistent(ExecutionContextInterface $context) + { + if ($this->getRole()->getType()->getId() !== + $this->getEvent()->getType()->getId()) { + $context->buildViolation('The role is not allowed with this event type') + ->atPath('role') + ->addViolation(); + } + + if ($this->getStatus()->getType()->getId() !== + $this->getEvent()->getType()->getId()) { + $context->buildViolation('The status is not allowed with this event type') + ->atPath('status') + ->addViolation(); + } + } + } diff --git a/Form/ParticipationType.php b/Form/ParticipationType.php new file mode 100644 index 000000000..4891a4811 --- /dev/null +++ b/Form/ParticipationType.php @@ -0,0 +1,117 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\EventBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Chill\EventBundle\Entity\EventType; +use Chill\EventBundle\Entity\Status; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Doctrine\ORM\EntityRepository; +use Chill\EventBundle\Entity\Role; +use Chill\MainBundle\Templating\TranslatableStringHelper; + +/** + * A type to create a participation + * + * If the `event` option is defined, the role will be restricted + * + * @author Julien Fastré + */ +class ParticipationType extends AbstractType +{ + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + public function __construct(TranslatableStringHelper $translatableStringHelper) + { + $this->translatableStringHelper = $translatableStringHelper; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + // local copy of variable for Closure + $translatableStringHelper = $this->translatableStringHelper; + + // add role + $builder->add('role', EntityType::class, array( + 'class' => Role::class, + 'query_builder' => function (EntityRepository $er) use ($options) { + $qb = $er->createQueryBuilder('r'); + + if ($options['event_type'] instanceof EventType) { + $qb->where($qb->expr()->eq('r.type', ':event_type')) + ->setParameter('event_type', $options['event_type']); + } + + $qb->andWhere($qb->expr()->eq('r.active', ':active')) + ->setParameter('active', true); + + return $qb; + }, + 'choice_attr' => function(Role $r) { + return array( + 'data-event-type' => $r->getType()->getId() + ); + }, + 'choice_label' => function(Role $r) use ($translatableStringHelper) { + return $translatableStringHelper->localize($r->getLabel()); + } + )); + + // add a status + $builder->add('status', EntityType::class, array( + 'class' => Status::class, + 'choice_attr' => function(Status $s) { + return array( + 'data-event-type' => $s->getType()->getId() + ); + }, + 'query_builder' => function (EntityRepository $er) use ($options) { + $qb = $er->createQueryBuilder('s'); + + if ($options['event_type'] instanceof EventType) { + $qb->where($qb->expr()->eq('s.type', ':event_type')) + ->setParameter('event_type', $options['event_type']); + } + + $qb->andWhere($qb->expr()->eq('s.active', ':active')) + ->setParameter('active', true); + + return $qb; + }, + 'choice_label' => function(Status $s) use ($translatableStringHelper) { + return $translatableStringHelper->localize($s->getLabel()); + } + )); + + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefined('event_type') + ->setAllowedTypes('event_type', array('null', EventType::class)) + ->setDefault('event_type', 'null'); + } +} diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 0f58caa77..c8be2882e 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -13,3 +13,7 @@ chill_event_admin_role: chill_event_admin_event_type: resource: "@ChillEventBundle/Resources/config/routing/eventtype.yml" prefix: /{_locale}/admin/event/event_type + +chill_event_participation: + resource: "@ChillEventBundle/Resources/config/routing/participation.yml" + prefix: /{_locale}/event/participation diff --git a/Resources/config/routing/participation.yml b/Resources/config/routing/participation.yml new file mode 100644 index 000000000..f79e584a6 --- /dev/null +++ b/Resources/config/routing/participation.yml @@ -0,0 +1,7 @@ +chill_event_participation_new: + path: /new + defaults: { _controller: ChillEventBundle:Participation:new } + +chill_event_participation_create: + path: /create + defaults: { _controller: ChillEventBundle:Participation:create } diff --git a/Resources/config/services/authorization.yml b/Resources/config/services/authorization.yml index 9adb5b087..d44f1e9d9 100644 --- a/Resources/config/services/authorization.yml +++ b/Resources/config/services/authorization.yml @@ -6,3 +6,11 @@ services: tags: - { name: chill.role } - { name: security.voter } + + chill_event.event_participation: + class: Chill\EventBundle\Security\Authorization\ParticipationVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: chill.role } + - { name: security.voter } diff --git a/Resources/config/services/forms.yml b/Resources/config/services/forms.yml index 2f1cb1d00..2f622a326 100644 --- a/Resources/config/services/forms.yml +++ b/Resources/config/services/forms.yml @@ -14,3 +14,10 @@ services: - "@chill.main.helper.translatable_string" tags: - { name: form.type } + + chill.event.form.participation_type: + class: Chill\EventBundle\Form\ParticipationType + arguments: + - "@chill.main.helper.translatable_string" + tags: + - { name: form.type } diff --git a/Resources/config/validation.yml b/Resources/config/validation.yml new file mode 100644 index 000000000..90ebf28e0 --- /dev/null +++ b/Resources/config/validation.yml @@ -0,0 +1,10 @@ +Chill\EventBundle\Event\Participation: + properties: + event: + - NotNull: ~ + status: + - NotNull: ~ + person: + - NotNull: ~ + constraints: + - Callback: [isConsistent] diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 90a759f7c..e5d2a5e98 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -12,8 +12,13 @@ Last update: Dernière mise à jour #CRUD Details of an event: Détails d'un événement - Edit all the participations: Modifier toutes les participations +Add a participation: Ajouter un participant +Participation creation: Ajouter une participation +Associated person: Personne associée +Associated event: Événement associé +Back to the event: Retour à l'événement +The participation was created: La participation a été créée #search Event search: Recherche d'événements diff --git a/Resources/views/Event/show.html.twig b/Resources/views/Event/show.html.twig index 1a500381a..720075281 100644 --- a/Resources/views/Event/show.html.twig +++ b/Resources/views/Event/show.html.twig @@ -36,7 +36,7 @@ -

{{ 'Participation'|trans }}

+

{{ 'Participations'|trans }}

{% set count = event.participations|length %}

{% transchoice count %}%count% participations to this event{% endtranschoice %}

@@ -74,6 +74,14 @@
  • {{ 'Edit all the participations'|trans }}
  • +
  • + {{ form_start(form_add_participation_by_person) }} + {{ form_widget(form_add_participation_by_person.person_id, { 'attr' : { 'style' : 'width: 20em; display:inline-block; ' } } ) }} + {{ form_widget(form_add_participation_by_person.submit, { 'attr' : { 'class' : 'sc-button btn-create' } } ) }} + {{ form_rest(form_add_participation_by_person) }} + {{ form_end(form_add_participation_by_person) }} +
    +
{% endif %} diff --git a/Resources/views/Participation/new.html.twig b/Resources/views/Participation/new.html.twig new file mode 100644 index 000000000..bd99347fe --- /dev/null +++ b/Resources/views/Participation/new.html.twig @@ -0,0 +1,37 @@ +{% extends 'ChillEventBundle::layout.html.twig' %} + +{% import 'ChillPersonBundle:Person:macro.html.twig' as person_macro %} + +{% block event_content -%} +

{{ 'Participation creation'|trans }}

+ + + + + + + + + + + + +
{{ 'Associated person'|trans }}{{ person_macro.render(participation.person) }}
{{ 'Associated event'|trans }} {{ participation.event.label }}
+ + {{ form_start(form) }} + {{ form_row(form.role) }} + {{ form_row(form.status) }} + + + + {{ form_end(form) }} +{% endblock %} diff --git a/Security/Authorization/ParticipationVoter.php b/Security/Authorization/ParticipationVoter.php new file mode 100644 index 000000000..e8ba27548 --- /dev/null +++ b/Security/Authorization/ParticipationVoter.php @@ -0,0 +1,82 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\EventBundle\Security\Authorization; + +use Chill\MainBundle\Security\ProvideRoleInterface; +use Chill\MainBundle\Security\Authorization\AbstractChillVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\EventBundle\Entity\Participation; +use Chill\MainBundle\Entity\User; + +/** + * + * + * @author Julien Fastré + */ +class ParticipationVoter extends AbstractChillVoter implements ProvideRoleInterface +{ + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + const CREATE = 'CHILL_EVENT_PARTICIPATION_CREATE'; + const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE'; + + public function __construct(AuthorizationHelper $helper) + { + $this->authorizationHelper = $helper; + } + + protected function getSupportedAttributes() + { + return array( + self::CREATE, self::UPDATE + ); + } + + protected function getSupportedClasses() + { + return array( + Participation::class + ); + } + + protected function isGranted($attribute, $participation, $user = null) + { + if (!$user instanceof User) { + return false; + } + + return $this->authorizationHelper->userHasAccess($user, $participation, $attribute); + } + + public function getRoles() + { + return $this->getSupportedAttributes(); + } + + public function getRolesWithoutScope() + { + return null; + } + +}