diff --git a/docs/source/development/timelines.rst b/docs/source/development/timelines.rst index a8d0ff0be..afabdb398 100644 --- a/docs/source/development/timelines.rst +++ b/docs/source/development/timelines.rst @@ -97,7 +97,7 @@ The has the following signature : * * @param string $context * @param mixed[] $args the argument to the context. - * @return string[] + * @return TimelineSingleQuery * @throw \LogicException if the context is not supported */ public function fetchQuery($context, array $args); @@ -163,18 +163,16 @@ The has the following signature : The `fetchQuery` function ^^^^^^^^^^^^^^^^^^^^^^^^^ -The fetchQuery function help to build the UNION query to gather events. This function should return an associative array MUST have the following key : +The fetchQuery function help to build the UNION query to gather events. This function should return an instance of :code:`TimelineSingleQuery`. For you convenience, this object may be build using an associative array with the following keys: * `id` : the name of the id column * `type`: a string to indicate the type * `date`: the name of the datetime column, used to order entities by date -* `FROM` (in capital) : the FROM clause. May contains JOIN instructions +* `FROM`: the FROM clause. May contains JOIN instructions +* `WHERE`: the WHERE clause; +* `parameters`: the parameters to pass to the query -Those key are optional: - -* `WHERE` (in capital) : the WHERE clause. - - Where relevant, the data must be quoted to avoid SQL injection. +The parameters should be replaced into the query by :code:`?`. They will be replaced into the query using prepared statements. `$context` and `$args` are defined by the bundle which will call the timeline rendering. You may use them to build a different query depending on this context. @@ -186,6 +184,15 @@ For instance, if the context is `'person'`, the args will be this array : 'person' => $person //a \Chill\PersonBundle\Entity\Person entity ); +For the context :code:`center`, the args will be: + +.. code-block:: php + + array( + 'centers' => [ ] // an array of \Chill\MainBundle\Entity\Center entities + ); + + You should find in the bundle documentation which contexts are arguments the bundle defines. .. note:: @@ -199,13 +206,12 @@ Example of an implementation : namespace Chill\ReportBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; + use Chill\MainBundle\Timeline\TimelineSingleQuery; use Doctrine\ORM\EntityManager; /** * Provide report for inclusion in timeline * - * @author Julien Fastré - * @author Champs Libres */ class TimelineReportProvider implements TimelineProviderInterface { @@ -227,16 +233,17 @@ Example of an implementation : $metadata = $this->em->getClassMetadata('ChillReportBundle:Report'); - return array( + return TimelineSingleQuery::fromArray([ 'id' => $metadata->getColumnName('id'), 'type' => 'report', 'date' => $metadata->getColumnName('date'), 'FROM' => $metadata->getTableName(), - 'WHERE' => sprintf('%s = %d', + 'WHERE' => sprintf('%s = ?', $metadata - ->getAssociationMapping('person')['joinColumns'][0]['name'], - $args['person']->getId()) - ); + ->getAssociationMapping('person')['joinColumns'][0]['name']) + ) + 'parameters' => [ $args['person']->getId() ] + ]); } //.... diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index e96db03db..afa631761 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -38,7 +38,7 @@ use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency; * Class Activity * * @package Chill\ActivityBundle\Entity - * @ORM\Entity() + * @ORM\Entity(repositoryClass="Chill\ActivityBundle\Repository\ActivityRepository") * @ORM\Table(name="activity") * @ORM\HasLifecycleCallbacks() * @UserCircleConsistency( diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php new file mode 100644 index 000000000..b198875c5 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -0,0 +1,169 @@ +, + * + * 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\ActivityBundle\Repository; + +use Chill\ActivityBundle\Entity\Activity; +use Chill\PersonBundle\Entity\Person; +use Chill\ActivityBundle\Repository\ActivityRepository; +use Chill\ActivityBundle\Security\Authorization\ActivityVoter; +use Chill\MainBundle\Entity\Scope; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\Expr\Orx; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\Role; +use Doctrine\ORM\EntityManagerInterface; + + +final class ActivityACLAwareRepository +{ + private AuthorizationHelper $authorizationHelper; + + private TokenStorageInterface $tokenStorage; + + private ActivityRepository $repository; + + private EntityManagerInterface $em; + + public function __construct( + AuthorizationHelper $authorizationHelper, + TokenStorageInterface $tokenStorage, + ActivityRepository $repository, + EntityManagerInterface $em + ) { + $this->authorizationHelper = $authorizationHelper; + $this->tokenStorage = $tokenStorage; + $this->repository = $repository; + $this->em = $em; + } + + public function queryTimelineIndexer(string $context, array $args = []): array + { + $metadataActivity = $this->em->getClassMetadata(Activity::class); + + $from = $this->getFromClauseCenter($args); + [$where, $parameters] = $this->getWhereClause($context, $args); + + return [ + 'id' => $metadataActivity->getTableName() + .'.'.$metadataActivity->getColumnName('id'), + 'type' => 'activity', + 'date' => $metadataActivity->getTableName() + .'.'.$metadataActivity->getColumnName('date'), + 'FROM' => $from, + 'WHERE' => $where, + 'parameters' => $parameters + ]; + } + + private function getFromClauseCenter(array $args): string + { + $metadataActivity = $this->em->getClassMetadata(Activity::class); + $metadataPerson = $this->em->getClassMetadata(Person::class); + $associationMapping = $metadataActivity->getAssociationMapping('person'); + + return $metadataActivity->getTableName().' JOIN ' + .$metadataPerson->getTableName().' ON ' + .$metadataPerson->getTableName().'.'. + $associationMapping['joinColumns'][0]['referencedColumnName'] + .' = ' + .$associationMapping['joinColumns'][0]['name'] + ; + } + + private function getWhereClause(string $context, array $args): array + { + $where = ''; + $parameters = []; + + $metadataActivity = $this->em->getClassMetadata(Activity::class); + $metadataPerson = $this->em->getClassMetadata(Person::class); + $activityToPerson = $metadataActivity->getAssociationMapping('person')['joinColumns'][0]['name']; + $activityToScope = $metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name']; + $personToCenter = $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name']; + + + // acls: + $role = new Role(ActivityVoter::SEE); + $reachableCenters = $this->authorizationHelper->getReachableCenters($this->tokenStorage->getToken()->getUser(), + $role); + + if (count($reachableCenters) === 0) { + // insert a dummy condition + return 'FALSE = TRUE'; + } + + if ($context === 'person') { + // we start with activities having the person_id linked to person + $where .= sprintf('%s = ? AND ', $activityToPerson); + $parameters[] = $person->getId(); + } + + // we add acl (reachable center and scopes) + $where .= '('; // first loop for the for centers + $centersI = 0; // like centers#i + foreach ($reachableCenters as $center) { + // we pass if not in centers + if (!\in_array($center, $args['centers'])) { + continue; + } + // we get all the reachable scopes for this center + $reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), $role, $center); + // we get the ids for those scopes + $reachablesScopesId = array_map( + function(Scope $scope) { return $scope->getId(); }, + $reachableScopes + ); + + // if not the first center + if ($centersI > 0) { + $where .= ') OR ('; + } + + // condition for the center + $where .= sprintf(' %s.%s = ? ', $metadataPerson->getTableName(), $personToCenter); + $parameters[] = $center->getId(); + + // begin loop for scopes + $where .= ' AND ('; + $scopesI = 0; //like scope#i + + foreach ($reachablesScopesId as $scopeId) { + if ($scopesI > 0) { + $where .= ' OR '; + } + $where .= sprintf(' %s.%s = ? ', $metadataActivity->getTableName(), $activityToScope); + $parameters[] = $scopeId; + $scopesI ++; + } + // close loop for scopes + $where .= ') '; + $centersI++; + } + // close loop for centers + $where .= ')'; + + return [$where, $parameters]; + } + +} diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php new file mode 100644 index 000000000..b5155d4c9 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -0,0 +1,42 @@ +, + * + * 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\ActivityBundle\Repository; + +use Chill\ActivityBundle\Entity\Activity; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @method AccompanyingPeriodParticipation|null find($id, $lockMode = null, $lockVersion = null) + * @method AccompanyingPeriodParticipation|null findOneBy(array $criteria, array $orderBy = null) + * @method AccompanyingPeriodParticipation[] findAll() + * @method AccompanyingPeriodParticipation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ActivityRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Activity::class); + } + +} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Timeline/activity_person_context.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Timeline/activity_person_context.html.twig index dddd05cf7..c968c889b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Timeline/activity_person_context.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Timeline/activity_person_context.html.twig @@ -1,11 +1,11 @@ {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
-

{{ activity.date|format_date('long') }} / {{ 'Activity'|trans }}

+

{{ activity.date|format_date('long') }} / {{ 'Activity'|trans }}{% if 'person' != context %} / {{ activity.person|chill_entity_render_box({'addLink': true}) }}{% endif %}

{{ '%user% has done an %activity_type%'|trans( { - '%user%' : user, + '%user%' : activity.user, '%activity_type%': activity.type.name|localize_translatable_string, '%date%' : activity.date|format_date('long') } ) }} @@ -29,13 +29,13 @@
  • - + {{ 'Show the activity'|trans }}
  • {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
  • - + {{ 'Edit the activity'|trans }}
  • diff --git a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php index c87a17af4..a1c6b6c65 100644 --- a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php +++ b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php @@ -21,6 +21,7 @@ namespace Chill\ActivityBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; +use Chill\ActivityBundle\Repository\ActivityACLAwareRepository; use Doctrine\ORM\EntityManager; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -28,13 +29,13 @@ use Symfony\Component\Security\Core\Role\Role; use Doctrine\ORM\Mapping\ClassMetadata; use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Entity\Scope; +use Chill\ActivityBundle\Entity\Activity; +use Chill\MainBundle\Timeline\TimelineSingleQuery; /** * Provide activity for inclusion in timeline * - * @author Julien Fastré - * @author Champs Libres - */ +*/ class TimelineActivityProvider implements TimelineProviderInterface { @@ -55,6 +56,10 @@ class TimelineActivityProvider implements TimelineProviderInterface * @var \Chill\MainBundle\Entity\User */ protected $user; + + protected ActivityACLAwareRepository $aclAwareRepository; + + private const SUPPORTED_CONTEXTS = [ 'center', 'person']; /** * TimelineActivityProvider constructor. @@ -66,11 +71,13 @@ class TimelineActivityProvider implements TimelineProviderInterface public function __construct( EntityManager $em, AuthorizationHelper $helper, - TokenStorageInterface $storage + TokenStorageInterface $storage, + ActivityACLAwareRepository $aclAwareRepository ) { $this->em = $em; $this->helper = $helper; + $this->aclAwareRepository = $aclAwareRepository; if (!$storage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User) { @@ -86,67 +93,69 @@ class TimelineActivityProvider implements TimelineProviderInterface */ public function fetchQuery($context, array $args) { - $this->checkContext($context); + if ('center' === $context) { + return TimelineSingleQuery::fromArray($this->aclAwareRepository + ->queryTimelineIndexer($context, $args)); + } - $metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity'); - $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); + $metadataActivity = $this->em->getClassMetadata(Activity::class); + + [$where, $parameters] = $this->getWhereClauseForPerson($args['person']); - return array( + return TimelineSingleQuery::fromArray([ 'id' => $metadataActivity->getTableName() .'.'.$metadataActivity->getColumnName('id'), 'type' => 'activity', 'date' => $metadataActivity->getTableName() .'.'.$metadataActivity->getColumnName('date'), - 'FROM' => $this->getFromClause($metadataActivity, $metadataPerson), - 'WHERE' => $this->getWhereClause($metadataActivity, $metadataPerson, - $args['person']) - ); + 'FROM' => $this->getFromClausePerson($args['person']), + 'WHERE' => $where, + 'parameters' => $parameters + ]); } - private function getWhereClause(ClassMetadata $metadataActivity, - ClassMetadata $metadataPerson, Person $person) + private function getWhereClauseForPerson(Person $person) { - $role = new Role('CHILL_ACTIVITY_SEE'); - $reachableCenters = $this->helper->getReachableCenters($this->user, - $role); + $parameters = []; + $metadataActivity = $this->em->getClassMetadata(Activity::class); $associationMapping = $metadataActivity->getAssociationMapping('person'); - - if (count($reachableCenters) === 0) { - return 'FALSE = TRUE'; + $role = new Role('CHILL_ACTIVITY_SEE'); + $reachableScopes = $this->helper->getReachableScopes($this->user, + $role, $person->getCenter()); + $whereClause = sprintf(' {activity.person_id} = ? AND {activity.scope_id} IN ({scopes_ids}) '); + $scopes_ids = []; + + // first parameter: activity.person_id + $parameters[] = $person->getId(); + + // loop on reachable scopes + foreach ($reachableScopes as $scope) { + if (\in_array($scope->getId(), $scopes_ids)) { + continue; + } + $scopes_ids[] = '?'; + $parameters[] = $scope->getId(); } - - // we start with activities having the person_id linked to person - // (currently only context "person" is supported) - $whereClause = sprintf('%s = %d', - $associationMapping['joinColumns'][0]['name'], - $person->getId()); - - // we add acl (reachable center and scopes) - $centerAndScopeLines = array(); - foreach ($reachableCenters as $center) { - $reachablesScopesId = array_map( - function(Scope $scope) { return $scope->getId(); }, - $this->helper->getReachableScopes($this->user, $role, - $person->getCenter()) - ); - - $centerAndScopeLines[] = sprintf('(%s = %d AND %s IN (%s))', - $metadataPerson->getTableName().'.'. - $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'], - $center->getId(), - $metadataActivity->getTableName().'.'. + + return [ + \strtr( + $whereClause, + [ + '{activity.person_id}' => $associationMapping['joinColumns'][0]['name'], + '{activity.scope_id}' => $metadataActivity->getTableName().'.'. $metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name'], - implode(',', $reachablesScopesId)); - - } - $whereClause .= ' AND ('.implode(' OR ', $centerAndScopeLines).')'; - - return $whereClause; + '{scopes_ids}' => \implode(", ", $scopes_ids) +, + ] + ), + $parameters + ]; } - private function getFromClause(ClassMetadata $metadataActivity, - ClassMetadata $metadataPerson) + private function getFromClausePerson() { + $metadataActivity = $this->em->getClassMetadata(Activity::class); + $metadataPerson = $this->em->getClassMetadata(Person::class); $associationMapping = $metadataActivity->getAssociationMapping('person'); return $metadataActivity->getTableName().' JOIN ' @@ -157,14 +166,14 @@ class TimelineActivityProvider implements TimelineProviderInterface .$associationMapping['joinColumns'][0]['name'] ; } - + /** * * {@inheritDoc} */ public function getEntities(array $ids) { - $activities = $this->em->getRepository('ChillActivityBundle:Activity') + $activities = $this->em->getRepository(Activity::class) ->findBy(array('id' => $ids)); $result = array(); @@ -183,14 +192,13 @@ class TimelineActivityProvider implements TimelineProviderInterface { $this->checkContext($context); - return array( + return [ 'template' => 'ChillActivityBundle:Timeline:activity_person_context.html.twig', - 'template_data' => array( + 'template_data' => [ 'activity' => $entity, - 'person' => $args['person'], - 'user' => $entity->getUser() - ) - ); + 'context' => $context + ] + ]; } /** @@ -210,7 +218,7 @@ class TimelineActivityProvider implements TimelineProviderInterface */ private function checkContext($context) { - if ($context !== 'person') { + if (FALSE === \in_array($context, self::SUPPORTED_CONTEXTS)) { throw new \LogicException("The context '$context' is not " . "supported. Currently only 'person' is supported"); } diff --git a/src/Bundle/ChillActivityBundle/config/services.yaml b/src/Bundle/ChillActivityBundle/config/services.yaml index f83f834d1..411ad1b5a 100644 --- a/src/Bundle/ChillActivityBundle/config/services.yaml +++ b/src/Bundle/ChillActivityBundle/config/services.yaml @@ -22,9 +22,11 @@ services: - '@doctrine.orm.entity_manager' - '@chill.main.security.authorization.helper' - '@security.token_storage' + - '@Chill\ActivityBundle\Repository\ActivityACLAwareRepository' public: true tags: - { name: chill.timeline, context: 'person' } + - { name: chill.timeline, context: 'center' } Chill\ActivityBundle\Menu\: autowire: true diff --git a/src/Bundle/ChillActivityBundle/config/services/repositories.yaml b/src/Bundle/ChillActivityBundle/config/services/repositories.yaml index 2867782b4..2f0a9b83c 100644 --- a/src/Bundle/ChillActivityBundle/config/services/repositories.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/repositories.yaml @@ -1,18 +1,32 @@ +--- services: chill_activity.repository.activity_type: class: Doctrine\ORM\EntityRepository factory: ['@doctrine.orm.entity_manager', getRepository] arguments: - 'Chill\ActivityBundle\Entity\ActivityType' - + chill_activity.repository.reason: class: Doctrine\ORM\EntityRepository factory: ['@doctrine.orm.entity_manager', getRepository] arguments: - 'Chill\ActivityBundle\Entity\ActivityReason' - + chill_activity.repository.reason_category: class: Doctrine\ORM\EntityRepository factory: ['@doctrine.orm.entity_manager', getRepository] arguments: - 'Chill\ActivityBundle\Entity\ActivityReasonCategory' + + Chill\ActivityBundle\Repository\ActivityRepository: + tags: [doctrine.repository_service] + arguments: + - '@Doctrine\Persistence\ManagerRegistry' + + Chill\ActivityBundle\Repository\ActivityACLAwareRepository: + arguments: + $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + $repository: '@Chill\ActivityBundle\Repository\ActivityRepository' + $em: '@Doctrine\ORM\EntityManagerInterface' + diff --git a/src/Bundle/ChillEventBundle/Timeline/TimelineEventProvider.php b/src/Bundle/ChillEventBundle/Timeline/TimelineEventProvider.php index 3e7c67562..ffb068d70 100644 --- a/src/Bundle/ChillEventBundle/Timeline/TimelineEventProvider.php +++ b/src/Bundle/ChillEventBundle/Timeline/TimelineEventProvider.php @@ -23,6 +23,7 @@ namespace Chill\EventBundle\Timeline; use Chill\EventBundle\Entity\Event; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Timeline\TimelineProviderInterface; +use Chill\MainBundle\Timeline\TimelineSingleQuery; use Doctrine\ORM\EntityManager; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -88,13 +89,14 @@ class TimelineEventProvider implements TimelineProviderInterface $metadataParticipation = $this->em->getClassMetadata('ChillEventBundle:Participation'); $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); - $query = array( + $query = TimelineSingleQuery::fromArray([ 'id' => $metadataEvent->getTableName().'.'.$metadataEvent->getColumnName('id'), 'type' => 'event', 'date' => $metadataEvent->getTableName().'.'.$metadataEvent->getColumnName('date'), 'FROM' => $this->getFromClause($metadataEvent, $metadataParticipation, $metadataPerson), - 'WHERE' => $this->getWhereClause($metadataEvent, $metadataParticipation, $metadataPerson, $args['person']) - ); + 'WHERE' => $this->getWhereClause($metadataEvent, $metadataParticipation, $metadataPerson, $args['person']), + 'parameters' => [] + ]); return $query; } @@ -238,4 +240,4 @@ class TimelineEventProvider implements TimelineProviderInterface ); } -} \ No newline at end of file +} diff --git a/src/Bundle/ChillMainBundle/Controller/TimelineCenterController.php b/src/Bundle/ChillMainBundle/Controller/TimelineCenterController.php new file mode 100644 index 000000000..af17993ed --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/TimelineCenterController.php @@ -0,0 +1,91 @@ + + * + * 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\MainBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; +use Chill\MainBundle\Timeline\TimelineBuilder; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; + +class TimelineCenterController extends AbstractController +{ + + protected TimelineBuilder $timelineBuilder; + + protected PaginatorFactory $paginatorFactory; + + private Security $security; + + public function __construct( + TimelineBuilder $timelineBuilder, + PaginatorFactory $paginatorFactory, + Security $security + ) { + $this->timelineBuilder = $timelineBuilder; + $this->paginatorFactory = $paginatorFactory; + $this->security = $security; + } + + /** + * @Route("/{_locale}/center/timeline", + * name="chill_center_timeline", + * methods={"GET"} + * ) + */ + public function centerAction(Request $request) + { + // collect reachable center for each group + $user = $this->security->getUser(); + $centers = []; + foreach ($user->getGroupCenters() as $group) { + $centers[] = $group->getCenter(); + } + + if (0 === count($centers)) { + throw $this->createNotFoundException(); + } + + $nbItems = $this->timelineBuilder->countItems('center', + [ 'centers' => $centers ] + ); + + $paginator = $this->paginatorFactory->create($nbItems); + + return $this->render('@ChillMain/Timeline/index.html.twig', array + ( + 'timeline' => $this->timelineBuilder->getTimelineHTML( + 'center', + [ 'centers' => $centers ], + $paginator->getCurrentPage()->getFirstItemNumber(), + $paginator->getItemsPerPage() + ), + 'nb_items' => $nbItems, + 'paginator' => $paginator + ) + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Timeline/chain_timelines.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Timeline/chain_timelines.html.twig new file mode 100644 index 000000000..89dda9f8c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Timeline/chain_timelines.html.twig @@ -0,0 +1,7 @@ +
    + {% for result in results %} +
    + {% include result.template with result.template_data %} +
    + {% endfor %} +
    diff --git a/src/Bundle/ChillMainBundle/Resources/views/Timeline/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Timeline/index.html.twig index 89dda9f8c..d3a5f0780 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Timeline/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Timeline/index.html.twig @@ -1,7 +1,15 @@ -
    - {% for result in results %} -
    - {% include result.template with result.template_data %} -
    - {% endfor %} -
    +{% extends "@ChillMain/layout.html.twig" %} + +{% block content %} +
    +
    +

    {{ 'Global timeline'|trans }}

    + + {{ timeline|raw }} + + {% if nb_items > paginator.getItemsPerPage %} + {{ chill_pagination(paginator) }} + {% endif %} +
    +
    + {% endblock content %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php index b52a0502e..b8619750c 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php @@ -68,6 +68,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface 'icons' => ['home'], 'order' => 0 ]); + + $menu->addChild($this->translator->trans('Global timeline'), [ + 'route' => 'chill_center_timeline', + ]) + ->setExtras([ + 'order' => 10 + ] + ); if ($this->authorizationChecker->isGranted(ChillExportVoter::EXPORT)) { $menu->addChild($this->translator->trans('Export Menu'), [ diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index ed2dd499d..697158bf8 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -110,8 +110,6 @@ class AuthorizationHelper return false; } - $role = ($attribute instanceof Role) ? $attribute : new Role($attribute); - foreach ($user->getGroupCenters() as $groupCenter){ //filter on center if ($groupCenter->getCenter()->getId() === $entity->getCenter()->getId()) { @@ -119,8 +117,7 @@ class AuthorizationHelper //iterate on roleScopes foreach($permissionGroup->getRoleScopes() as $roleScope) { //check that the role allow to reach the required role - if ($this->isRoleReached($role, - new Role($roleScope->getRole()))){ + if ($this->isRoleReached($attribute, $roleScope->getRole())) { //if yes, we have a right on something... // perform check on scope if necessary if ($entity instanceof HasScopeInterface) { @@ -149,12 +146,15 @@ class AuthorizationHelper * and optionnaly Scope * * @param User $user - * @param Role $role + * @param string|Role $role * @param null|Scope $scope * @return Center[] */ - public function getReachableCenters(User $user, Role $role, Scope $scope = null) + public function getReachableCenters(User $user, $role, Scope $scope = null) { + if ($role instanceof Role) { + $role = $role->getRole(); + } $centers = array(); foreach ($user->getGroupCenters() as $groupCenter){ @@ -162,8 +162,7 @@ class AuthorizationHelper //iterate on roleScopes foreach($permissionGroup->getRoleScopes() as $roleScope) { //check that the role is in the reachable roles - if ($this->isRoleReached($role, - new Role($roleScope->getRole()))) { + if ($this->isRoleReached($role, $roleScope->getRole())) { if ($scope === null) { $centers[] = $groupCenter->getCenter(); break 1; @@ -180,6 +179,30 @@ class AuthorizationHelper return $centers; } + + /** + * Filter an array of centers, return only center which are reachable + * + * @param User $user The user + * @param array $centers a list of centers which are going to be filtered + * @param string|Center $role + */ + public function filterReachableCenters(User $user, array $centers, $role): array + { + $results = []; + + if ($role instanceof Role) { + $role = $role->getRole(); + } + + foreach ($centers as $center) { + if ($this->userCanReachCenter($user, $center, $role)) { + $results[] = $center; + } + } + + return $results; + } /** * Return all reachable scope for a given user, center and role @@ -191,8 +214,12 @@ class AuthorizationHelper * @param Center $center * @return Scope[] */ - public function getReachableScopes(User $user, Role $role, Center $center) + public function getReachableScopes(User $user, $role, Center $center) { + if ($role instanceof Role) { + $role = $role->getRole(); + } + return $this->getReachableCircles($user, $role, $center); } @@ -200,12 +227,15 @@ class AuthorizationHelper * Return all reachable circle for a given user, center and role * * @param User $user - * @param Role $role + * @param string|Role $role * @param Center $center * @return Scope[] */ - public function getReachableCircles(User $user, Role $role, Center $center) + public function getReachableCircles(User $user, $role, Center $center) { + if ($role instanceof Role) { + $role = $role->getRole(); + } $scopes = array(); foreach ($user->getGroupCenters() as $groupCenter){ @@ -215,9 +245,7 @@ class AuthorizationHelper //iterate on roleScopes foreach($permissionGroup->getRoleScopes() as $roleScope) { //check that the role is in the reachable roles - if ($this->isRoleReached($role, - new Role($roleScope->getRole()))) { - + if ($this->isRoleReached($role, $roleScope->getRole())) { $scopes[] = $roleScope->getScope(); } } @@ -269,10 +297,10 @@ class AuthorizationHelper * @param Role $parentRole The role which should give access to $childRole * @return boolean true if the child role is granted by parent role */ - protected function isRoleReached(Role $childRole, Role $parentRole) + protected function isRoleReached($childRole, $parentRole) { $reachableRoles = $this->roleHierarchy - ->getReachableRoles([$parentRole]); + ->getReachableRoleNames([$parentRole]); return in_array($childRole, $reachableRoles); } diff --git a/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php b/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php index 6d061d0a2..de2ae9975 100644 --- a/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php +++ b/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php @@ -23,11 +23,11 @@ use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Doctrine\ORM\Query; +use Doctrine\ORM\NativeQuery; /** * Build timeline - * - * @author Julien Fastré */ class TimelineBuilder implements ContainerAwareInterface { @@ -78,14 +78,14 @@ class TimelineBuilder implements ContainerAwareInterface */ public function getTimelineHTML($context, array $args, $firstItem = 0, $number = 20) { - $union = $this->buildUnionQuery($context, $args); + list($union, $parameters) = $this->buildUnionQuery($context, $args); //add ORDER BY clause and LIMIT $query = $union . sprintf(' ORDER BY date DESC LIMIT %d OFFSET %d', $number, $firstItem); // run query and handle results - $fetched = $this->runUnionQuery($query); + $fetched = $this->runUnionQuery($query, $parameters); $entitiesByKey = $this->getEntities($fetched, $context); return $this->render($fetched, $entitiesByKey, $context, $args); @@ -100,16 +100,18 @@ class TimelineBuilder implements ContainerAwareInterface */ public function countItems($context, array $args) { - $union = $this->buildUnionQuery($context, $args); - - // embed the union query inside a count query - $count = sprintf('SELECT COUNT(sq.id) AS total FROM (%s) as sq', $union); - $rsm = (new ResultSetMapping()) ->addScalarResult('total', 'total', Type::INTEGER); + + list($select, $parameters) = $this->buildUnionQuery($context, $args); + + // embed the union query inside a count query + $countQuery = sprintf('SELECT COUNT(sq.id) AS total FROM (%s) as sq', $select); + + $nq = $this->em->createNativeQuery($countQuery, $rsm); + $nq->setParameters($parameters); - return $this->em->createNativeQuery($count, $rsm) - ->getSingleScalarResult(); + return $nq->getSingleScalarResult(); } /** @@ -154,40 +156,59 @@ class TimelineBuilder implements ContainerAwareInterface * * @uses self::buildSelectQuery to build individual SELECT queries * - * @param string $context - * @param mixed $args - * @param int $page - * @param int $number - * @return string * @throws \LogicException if no builder have been defined for this context + * @return array, where first element is the query, the second one an array with the parameters */ - private function buildUnionQuery($context, array $args) + private function buildUnionQuery(string $context, array $args): array { //append SELECT queries with UNION keyword between them $union = ''; + $parameters = []; + foreach($this->getProvidersByContext($context) as $provider) { - $select = $this->buildSelectQuery($provider, $context, $args); - $append = ($union === '') ? $select : ' UNION '.$select; + $data = $provider->fetchQuery($context, $args); + list($select, $selectParameters) = $this->buildSelectQuery($data); + $append = empty($union) ? $select : ' UNION '.$select; $union .= $append; + $parameters = array_merge($parameters, $selectParameters); } - return $union; + return [$union, $parameters]; } + + /** + * Hack to replace the arbitrary "AS" statement in DQL + * into proper SQL query + * TODO remove + private function replaceASInDQL(string $dql): string + { + $pattern = '/^(SELECT\s+[a-zA-Z0-9\_\.\']{1,}\s+)(AS [a-z0-9\_]{1,})(\s{0,},\s{0,}[a-zA-Z0-9\_\.\']{1,}\s+)(AS [a-z0-9\_]{1,})(\s{0,},\s{0,}[a-zA-Z0-9\_\.\']{1,}\s+)(AS [a-z0-9\_]{1,})(\s+FROM.*)/'; + $replacements = '${1} AS id ${3} AS type ${5} AS date ${7}'; + + $s = \preg_replace($pattern, $replacements, $dql, 1); + + if (NULL === $s) { + throw new \RuntimeException('Could not replace the "AS" statement produced by '. + 'DQL with normal SQL AS: '.$dql); + } + + return $s; + } + */ /** * return the SQL SELECT query as a string, * - * @uses TimelineProfiderInterface::fetchQuery use the fetchQuery function - * @param \Chill\MainBundle\Timeline\TimelineProviderInterface $provider - * @param string $context - * @param mixed[] $args - * @return string + * @return array: first parameter is the sql string, second an array with parameters */ - private function buildSelectQuery(TimelineProviderInterface $provider, $context, array $args) + private function buildSelectQuery($data): array { - $data = $provider->fetchQuery($context, $args); - - return sprintf( + return [$data->buildSql(), $data->getParameters()]; + + // dead code + $parameters = []; + + $sql = sprintf( 'SELECT %s AS id, ' . '%s AS "date", ' . "'%s' AS type " @@ -197,16 +218,19 @@ class TimelineBuilder implements ContainerAwareInterface $data['date'], $data['type'], $data['FROM'], - $data['WHERE']); + is_string($data['WHERE']) ? $data['WHERE'] : $data['WHERE'][0] + ); + + return [$sql, $data['WHERE'][1]]; + } /** * run the UNION query and return result as an array * - * @param string $query - * @return array + * @return array an array with the results */ - private function runUnionQuery($query) + private function runUnionQuery(string $query, array $parameters): array { $resultSetMapping = (new ResultSetMapping()) ->addScalarResult('id', 'id') @@ -214,7 +238,8 @@ class TimelineBuilder implements ContainerAwareInterface ->addScalarResult('date', 'date'); return $this->em->createNativeQuery($query, $resultSetMapping) - ->getArrayResult(); + ->setParameters($parameters) + ->getArrayResult(); } /** @@ -274,7 +299,7 @@ class TimelineBuilder implements ContainerAwareInterface } return $this->container->get('templating') - ->render('@ChillMain/Timeline/index.html.twig', array( + ->render('@ChillMain/Timeline/chain_timelines.html.twig', array( 'results' => $timelineEntries )); diff --git a/src/Bundle/ChillMainBundle/Timeline/TimelineSingleQuery.php b/src/Bundle/ChillMainBundle/Timeline/TimelineSingleQuery.php new file mode 100644 index 000000000..e7e456a80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Timeline/TimelineSingleQuery.php @@ -0,0 +1,155 @@ +id = $id; + $this->date = $date; + $this->key = $key; + $this->from = $from; + $this->where = $where; + $this->parameters = $parameters; + } + + public static function fromArray(array $a) + { + return new TimelineSingleQuery( + $a['id'] ?? null, + $a['date'] ?? null, + $a['type'] ?? $a['key'] ?? null, + $a['FROM'] ?? $a['from'] ?? null, + $a['WHERE'] ?? $a['where'] ?? null, + $a['parameters'] ?? null); + } + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id): self + { + $this->id = $id; + + return $this; + } + + public function getDate(): string + { + return $this->date; + } + + public function setDate(string $date): self + { + $this->date = $date; + + return $this; + } + + public function getKey(): string + { + return $this->key; + } + + public function setKey(string $key): self + { + $this->key = $key; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getWhere(): string + { + return $this->where; + } + + public function setWhere(string $where): self + { + $this->where = $where; + + return $this; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function setParameters(array $parameters): self + { + $this->parameters = $parameters; + + return $this; + } + + public function setDistinct(bool $distinct): self + { + $this->distinct = $distinct; + + return $this; + } + + public function isDistinct(): bool + { + return $this->distinct; + } + + public function buildSql(): string + { + $parameters = []; + + $sql = \strtr( + 'SELECT {distinct} {id} AS id, ' + . '{date} AS "date", ' + . "'{key}' AS type " + . 'FROM {from} ' + . 'WHERE {where}', + [ + '{distinct}' => $this->distinct ? 'DISTINCT' : '', + '{id}' => $this->getId(), + '{date}' => $this->getDate(), + '{key}' => $this->getKey(), + '{from}' => $this->getFrom(), + '{where}' => $this->getWhere(), + ] + ); + + return $sql; + } +} diff --git a/src/Bundle/ChillMainBundle/config/routes.yaml b/src/Bundle/ChillMainBundle/config/routes.yaml index bc3187add..240a44590 100644 --- a/src/Bundle/ChillMainBundle/config/routes.yaml +++ b/src/Bundle/ChillMainBundle/config/routes.yaml @@ -1,3 +1,7 @@ +chill_main_controllers: + resource: '../Controller/' + type: annotation + chill_main_admin_permissionsgroup: resource: "@ChillMainBundle/config/routes/permissionsgroup.yaml" prefix: "{_locale}/admin/permissionsgroup" diff --git a/src/Bundle/ChillMainBundle/config/services/timeline.yaml b/src/Bundle/ChillMainBundle/config/services/timeline.yaml index 241f6a6f1..fe830c7ab 100644 --- a/src/Bundle/ChillMainBundle/config/services/timeline.yaml +++ b/src/Bundle/ChillMainBundle/config/services/timeline.yaml @@ -4,4 +4,7 @@ services: arguments: - "@doctrine.orm.entity_manager" calls: - - [ setContainer, ["@service_container"]] \ No newline at end of file + - [ setContainer, ["@service_container"]] + # alias: + Chill\MainBundle\Timeline\TimelineBuilder: '@chill_main.timeline_builder' + diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index a7b71a072..ecc595987 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -46,6 +46,11 @@ Back to the list: Retour à la liste #interval Years: Années +# misc date +Since %date%: Depuis le %date% +since %date%: depuis le %date% +Until %date%: Jusqu'au %date% +until %date%: jusqu'au %date% #elements used in software centers: centres Centers: Centres @@ -78,6 +83,9 @@ Results %start%-%end% of %total%: Résultats %start%-%end% sur %total% See all results: Voir tous les résultats Advanced search: Recherche avancée +# timeline +Global timeline: Historique global + #admin Create: Créer show: voir diff --git a/src/Bundle/ChillPersonBundle/Controller/TimelinePersonController.php b/src/Bundle/ChillPersonBundle/Controller/TimelinePersonController.php index 774351581..4e3d67755 100644 --- a/src/Bundle/ChillPersonBundle/Controller/TimelinePersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/TimelinePersonController.php @@ -27,32 +27,17 @@ use Symfony\Component\HttpFoundation\Request; use Chill\MainBundle\Timeline\TimelineBuilder; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Role\Role; -/** - * Class TimelinePersonController - * - * @package Chill\PersonBundle\Controller - * @author Julien Fastré - */ class TimelinePersonController extends AbstractController { - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; + protected EventDispatcherInterface $eventDispatcher; - /** - * - * @var TimelineBuilder - */ - protected $timelineBuilder; + protected TimelineBuilder $timelineBuilder; - /** - * - * @var PaginatorFactory - */ - protected $paginatorFactory; + protected PaginatorFactory $paginatorFactory; /** * TimelinePersonController constructor. @@ -62,11 +47,13 @@ class TimelinePersonController extends AbstractController public function __construct( EventDispatcherInterface $eventDispatcher, TimelineBuilder $timelineBuilder, - PaginatorFactory $paginatorFactory + PaginatorFactory $paginatorFactory, + AuthorizationHelper $authorizationHelper ) { $this->eventDispatcher = $eventDispatcher; $this->timelineBuilder = $timelineBuilder; $this->paginatorFactory = $paginatorFactory; + $this->authorizationHelper = $authorizationHelper; } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index c90f8b4f9..d0c5bea86 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -369,13 +369,9 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface return false; } - public function setRemark(string $remark): self + public function setRemark(string $remark = null): self { - if ($remark === null) { - $remark = ''; - } - - $this->remark = $remark; + $this->remark = (string) $remark; return $this; } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Timeline/closing_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Timeline/closing_period.html.twig index 27f13fc96..6c92a481a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Timeline/closing_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Timeline/closing_period.html.twig @@ -1,11 +1,24 @@ -

    +
    +

    {{ period.closingDate|format_date('long') }} - / - - {{ 'Closing the accompanying period' | trans }} - - - - - -

    + / + {{ 'An accompanying period ends'|trans }} + {% if 'person' != context %} + {% for p in period.persons %} + / {{ p|chill_entity_render_box({'addLink': true}) }} + {% endfor %} + {% endif %} +

    + +
    +
    +
    {{ 'Participants'|trans }} :
    +
    +
      + {% for p in period.participations %} +
    • {{ p.person|chill_entity_render_box({ 'addLink': true }) }}: {{ 'since %date%'|trans({'%date%': p.startDate|format_date("long") } ) }}, {{ 'until %date%'|trans({'%date%': (p.endDate is not null ? p.endDate : period.closingDate)|format_date("long") }) }}
    • + {% endfor %} +
    +
    +
    +
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Timeline/opening_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Timeline/opening_period.html.twig index e6fa5752c..e3df6b720 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Timeline/opening_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Timeline/opening_period.html.twig @@ -1,11 +1,24 @@ -

+
+

{{ period.openingDate|format_date('long') }} - / - - {{ 'Opening the accompanying period' | trans }} - - - - - -

+ / + {{ 'An accompanying period starts'|trans }} + {% if 'person' != context %} + {% for p in period.persons %} + / {{ p|chill_entity_render_box({'addLink': true}) }} + {% endfor %} + {% endif %} +

+ +
+
+
{{ 'Participants'|trans }} :
+
+
    + {% for p in period.participations %} +
  • {{ 'Since %date%'|trans( {'%date%': p.startDate|format_date("long") } ) }} : {{ p.person|chill_entity_render_box({ 'addLink': true }) }}
  • + {% endfor %} +
+
+
+
diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/PersonRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/PersonRender.php index d1f554492..717ee4fc6 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/PersonRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/PersonRender.php @@ -23,6 +23,8 @@ namespace Chill\PersonBundle\Templating\Entity; use Chill\MainBundle\Templating\Entity\AbstractChillEntityRender; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; +use Symfony\Component\Templating\EngineInterface; + /** * Render a Person @@ -30,15 +32,16 @@ use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; */ class PersonRender extends AbstractChillEntityRender { - /** - * - * @var ConfigPersonAltNamesHelper - */ - protected $configAltNamesHelper; + private ConfigPersonAltNamesHelper $configAltNamesHelper; + + private EngineInterface $engine; - public function __construct(ConfigPersonAltNamesHelper $configAltNamesHelper) - { + public function __construct( + ConfigPersonAltNamesHelper $configAltNamesHelper, + EngineInterface $engine + ) { $this->configAltNamesHelper = $configAltNamesHelper; + $this->engine = $engine; } /** @@ -49,13 +52,13 @@ class PersonRender extends AbstractChillEntityRender */ public function renderBox($person, array $options): string { - return - $this->getDefaultOpeningBox('person'). - ''.$person->getFirstName().''. - ' '.$person->getLastName().''. - $this->addAltNames($person, true). - $this->getDefaultClosingBox() - ; + return $this->engine->render('@ChillPerson/Entity/person.html.twig', + [ + 'person' => $person, + 'addAltNames' => $this->configAltNamesHelper->hasAltNames(), + 'addLink' => $options['addLink'] ?? false + ] + ); } /** @@ -69,7 +72,7 @@ class PersonRender extends AbstractChillEntityRender return $person->getFirstName().' '.$person->getLastName() .$this->addAltNames($person, false); } - + protected function addAltNames(Person $person, bool $addSpan) { $str = ''; diff --git a/src/Bundle/ChillPersonBundle/Timeline/AbstractTimelineAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Timeline/AbstractTimelineAccompanyingPeriod.php index 37f185bb2..d5778280a 100644 --- a/src/Bundle/ChillPersonBundle/Timeline/AbstractTimelineAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Timeline/AbstractTimelineAccompanyingPeriod.php @@ -21,6 +21,14 @@ namespace Chill\PersonBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; use Doctrine\ORM\EntityManager; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; +use Chill\MainBundle\Entity\Center; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Symfony\Component\Security\Core\Security; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Timeline\TimelineSingleQuery; /** * Provide method to build timeline for accompanying periods @@ -28,19 +36,22 @@ use Doctrine\ORM\EntityManager; * This class is resued by TimelineAccompanyingPeriodOpening (for opening) * and TimelineAccompanyingPeriodClosing (for closing) * - * @author Julien Fastré */ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInterface { - /** - * - * @var EntityManager - */ - protected $em; + protected EntityManager $em; + + private Security $security; + + private AuthorizationHelper $authorizationHelper; + + private const SUPPORTED_CONTEXTS = [ 'person', 'center' ]; - public function __construct(EntityManager $em) + public function __construct(EntityManager $em, Security $security, AuthorizationHelper $authorizationHelper) { $this->em = $em; + $this->security = $security; + $this->authorizationHelper = $authorizationHelper; } /** @@ -72,23 +83,74 @@ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInt */ protected function basicFetchQuery($context, array $args) { - if ($context !== 'person') { + if (FALSE === \in_array($context, self::SUPPORTED_CONTEXTS)) { throw new \LogicException('TimelineAccompanyingPeriod is not able ' . 'to render context '.$context); } $metadata = $this->em - ->getClassMetadata('ChillPersonBundle:AccompanyingPeriod') + ->getClassMetadata(AccompanyingPeriod::class) ; - - return array( - 'id' => $metadata->getColumnName('id'), - 'FROM' => $metadata->getTableName(), - 'WHERE' => sprintf('%s = %d', - $metadata - ->getAssociationMapping('person')['joinColumns'][0]['name'], - $args['person']->getId()) - ); + [$where, $parameters] = $this->buildWhereClause($context, $args); + + return TimelineSingleQuery::fromArray([ + 'id' => "{$metadata->getTableName()}.{$metadata->getColumnName('id')}", + 'FROM' => $this->buildFromClause($context), + 'WHERE' => $where, + 'parameters' => $parameters + ]); + } + + private function buildFromClause($context) + { + $period = $this->em->getClassMetadata(AccompanyingPeriod::class); + $participation = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class); + $person = $this->em->getClassMetadata(Person::class); + $join = $participation->getAssociationMapping('accompanyingPeriod')['joinColumns'][0]; + $joinPerson = $participation->getAssociationMapping('person')['joinColumns'][0]; + + if ($context === 'person') { + return "{$period->getTableName()} ". + "JOIN {$participation->getTableName()} ". + "ON {$participation->getTableName()}.{$join['name']} = ". + "{$period->getTableName()}.{$join['referencedColumnName']}"; + } else { + return "{$period->getTableName()} ". + "JOIN {$participation->getTableName()} ". + "ON {$participation->getTableName()}.{$join['name']} = ". + "{$period->getTableName()}.{$join['referencedColumnName']} ". + "JOIN {$person->getTableName()} ". + "ON {$participation->getTableName()}.{$joinPerson['name']} = ". + "{$person->getTableName()}.{$joinPerson['referencedColumnName']}" + ; + } + + } + + protected function buildWhereClause($context, array $args): array + { + $participation = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class); + $join = $participation->getAssociationMapping('person')['joinColumns'][0]; + $person = $this->em->getClassMetadata(Person::class); + $joinCenter = $person->getAssociationMapping('center')['joinColumns'][0]; + + if ($context === 'center') { + $allowedCenters = $this->authorizationHelper->filterReachableCenters($this->security->getUser(), $args['centers'], PersonVoter::SEE); + $params = []; + $questionMarks = []; + $query = "{$person->getTableName()}.{$joinCenter['name']} IN ("; + foreach ($allowedCenters as $c) { + $questionMarks[] = '?'; + $params[] = $c->getId(); + } + $query .= \implode(", ", $questionMarks).")"; + + return [$query, $params]; + } elseif ($context === 'person') { + return [ "{$participation->getTableName()}.{$join['name']} = ?", [ $args['person']->getId() ]]; + } + + throw new \LogicException("this context is not supported: $context"); } /** @@ -104,7 +166,7 @@ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInt { return array( 'template' => $template, - 'template_data' => ['person' => $args['person'], 'period' => $entity] + 'template_data' => ['period' => $entity, 'context' => $context] ); } } diff --git a/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodClosing.php b/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodClosing.php index 05aebbfd4..c873b6405 100644 --- a/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodClosing.php +++ b/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodClosing.php @@ -21,11 +21,10 @@ namespace Chill\PersonBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; use Doctrine\ORM\EntityManager; +use Chill\PersonBundle\Entity\AccompanyingPeriod; /** * Provide information for opening periods to timeline - * - * @author Julien Fastré */ class TimelineAccompanyingPeriodClosing extends AbstractTimelineAccompanyingPeriod { @@ -46,20 +45,27 @@ class TimelineAccompanyingPeriodClosing extends AbstractTimelineAccompanyingPeri public function fetchQuery($context, array $args) { $metadata = $this->em - ->getClassMetadata('ChillPersonBundle:AccompanyingPeriod'); + ->getClassMetadata(AccompanyingPeriod::class); - $data = $this->basicFetchQuery($context, $args); - - $data['type'] = 'accompanying_period_closing'; - $data['date'] = $metadata->getColumnName('closingDate'); - $data['WHERE'] = sprintf('%s = %d AND %s IS NOT NULL', - $metadata - ->getAssociationMapping('person')['joinColumns'][0]['name'], - $args['person']->getId(), - $metadata->getColumnName('closingDate')) + $query = $this->basicFetchQuery($context, $args); + [$where, $parameters] = $this->buildWhereClause($context, $args); + $query->setKey('accompanying_period_closing') + ->setDate($metadata->getColumnName('closingDate')) + ->setWhere($where) + ->setParameters($parameters) ; - return $data; + return $query; + } + + protected function buildWhereClause($context, array $args): array + { + list($query, $params) = parent::buildWhereClause($context, $args); + $period = $this->em->getClassMetadata(AccompanyingPeriod::class); + + $query .= " AND {$period->getColumnName('closingDate')} IS NOT NULL "; + + return [ $query, $params ]; } /** diff --git a/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodOpening.php b/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodOpening.php index f8789b088..04b3df5b8 100644 --- a/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodOpening.php +++ b/src/Bundle/ChillPersonBundle/Timeline/TimelineAccompanyingPeriodOpening.php @@ -21,11 +21,10 @@ namespace Chill\PersonBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; use Doctrine\ORM\EntityManager; +use Chill\PersonBundle\Entity\AccompanyingPeriod; /** * Provide information for opening periods to timeline - * - * @author Julien Fastré */ class TimelineAccompanyingPeriodOpening extends AbstractTimelineAccompanyingPeriod { @@ -46,14 +45,14 @@ class TimelineAccompanyingPeriodOpening extends AbstractTimelineAccompanyingPeri public function fetchQuery($context, array $args) { $metadata = $this->em - ->getClassMetadata('ChillPersonBundle:AccompanyingPeriod'); + ->getClassMetadata(AccompanyingPeriod::class); - $data = $this->basicFetchQuery($context, $args); + $query = $this->basicFetchQuery($context, $args); - $data['type'] = 'accompanying_period_opening'; - $data['date'] = $metadata->getColumnName('openingDate'); + $query->setKey('accompanying_period_opening') + ->setDate($metadata->getColumnName('openingDate')); - return $data; + return $query; } /** diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml index 523545900..0d1a37422 100644 --- a/src/Bundle/ChillPersonBundle/config/services.yaml +++ b/src/Bundle/ChillPersonBundle/config/services.yaml @@ -24,17 +24,23 @@ services: class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodOpening arguments: - "@doctrine.orm.entity_manager" + - '@Symfony\Component\Security\Core\Security' + - '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' public: true tags: - { name: chill.timeline, context: 'person' } + - { name: chill.timeline, context: 'center' } chill.person.timeline.accompanying_period_closing: class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodClosing arguments: - "@doctrine.orm.entity_manager" + - '@Symfony\Component\Security\Core\Security' + - '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' public: true tags: - { name: chill.timeline, context: 'person' } + - { name: chill.timeline, context: 'center' } chill.person.birthdate_validation: class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index 311950883..cbc77cce5 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -16,6 +16,7 @@ services: $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' $timelineBuilder: '@chill_main.timeline_builder' $paginatorFactory: '@chill_main.paginator_factory' + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' tags: ['controller.service_arguments'] Chill\PersonBundle\Controller\AccompanyingPeriodController: diff --git a/src/Bundle/ChillPersonBundle/config/services/templating.yaml b/src/Bundle/ChillPersonBundle/config/services/templating.yaml index 1e4fe4d3e..8b6e85a8c 100644 --- a/src/Bundle/ChillPersonBundle/config/services/templating.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/templating.yaml @@ -7,6 +7,7 @@ services: Chill\PersonBundle\Templating\Entity\PersonRender: arguments: $configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' + $engine: '@Symfony\Component\Templating\EngineInterface' tags: - 'chill.render_entity' diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 12ba14b05..701b76c9e 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -152,6 +152,8 @@ Update accompanying period: Mettre à jour une période d'accompagnement 'Closing motive': 'Motif de clôture' 'Person details': 'Détails de la personne' 'Update details for %name%': 'Modifier détails de %name%' +An accompanying period ends: Une periode d'accompagnement se clôture +An accompanying period starts: Une période d'accompagnement est ouverte Any accompanying periods are open: Aucune période d'accompagnement ouverte An accompanying period is open: Une période d'accompagnement est ouverte Accompanying period list: Périodes d'accompagnement @@ -176,11 +178,10 @@ See accompanying periods: Voir les périodes d'accompagnement See accompanying period: Voir cette période d'accompagnement Referrer: Référent +Participants: Personnes impliquées # pickAPersonType Pick a person: Choisir une personne -#address -Since %date%: Depuis le %date% No address given: Pas d'adresse renseignée The address has been successfully updated: L'adresse a été mise à jour avec succès Update address for %name%: Mettre à jour une adresse pour %name% diff --git a/src/Bundle/ChillReportBundle/Resources/views/Timeline/report_person_context.html.twig b/src/Bundle/ChillReportBundle/Resources/views/Timeline/report.html.twig similarity index 71% rename from src/Bundle/ChillReportBundle/Resources/views/Timeline/report_person_context.html.twig rename to src/Bundle/ChillReportBundle/Resources/views/Timeline/report.html.twig index e9bcba85b..7929fed9e 100644 --- a/src/Bundle/ChillReportBundle/Resources/views/Timeline/report_person_context.html.twig +++ b/src/Bundle/ChillReportBundle/Resources/views/Timeline/report.html.twig @@ -1,9 +1,9 @@
-

{{ report.date|format_date('long') }} / {{ 'Report'|trans }}

+

{{ report.date|format_date('long') }} / {{ 'Report'|trans }}{% if 'person' != context %} / {{ report.person|chill_entity_render_box }}{% endif %}

{{ '%user% has filled a %report_label% report'|trans( { - '%user%' : user, + '%user%' : report.user, '%report_label%': report.CFGroup.name|localize_translatable_string, '%date%' : report.date|format_date('long') } ) }} @@ -25,13 +25,13 @@
  • - + {{ 'View the report'|trans }}
  • {% if is_granted('CHILL_REPORT_UPDATE', report) %}
  • - + {{ 'Update the report'|trans }}
  • diff --git a/src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php b/src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php index c6c13173a..49e237d87 100644 --- a/src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php +++ b/src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php @@ -30,71 +30,34 @@ use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Entity\Scope; use Chill\CustomFieldsBundle\Service\CustomFieldsHelper; use Chill\ReportBundle\Entity\Report; +use Symfony\Component\Security\Core\Security; +use Chill\MainBundle\Timeline\TimelineSingleQuery; /** * Provide report for inclusion in timeline - * - * @author Julien Fastré - * @author Champs Libres */ class TimelineReportProvider implements TimelineProviderInterface { - /** - * - * @var EntityManager - */ - protected $em; + protected EntityManager $em; - /** - * - * @var AuthorizationHelper - */ - protected $helper; + protected AuthorizationHelper $helper; - /** - * - * @var \Chill\MainBundle\Entity\User - */ - protected $user; + protected CustomFieldsHelper $customFieldsHelper; - /** - * - * @var CustomFieldsHelper - */ - protected $customFieldsHelper; - - /** - * @var - */ protected $showEmptyValues; - /** - * TimelineReportProvider constructor. - * - * @param EntityManager $em - * @param AuthorizationHelper $helper - * @param TokenStorageInterface $storage - * @param CustomFieldsHelper $customFieldsHelper - * @param $showEmptyValues - */ public function __construct( EntityManager $em, AuthorizationHelper $helper, - TokenStorageInterface $storage, + Security $security, CustomFieldsHelper $customFieldsHelper, $showEmptyValues ) { $this->em = $em; $this->helper = $helper; - - if (!$storage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User) - { - throw new \RuntimeException('A user should be authenticated !'); - } - - $this->user = $storage->getToken()->getUser(); + $this->security = $security; $this->customFieldsHelper = $customFieldsHelper; $this->showEmptyValues = $showEmptyValues; } @@ -107,70 +70,162 @@ class TimelineReportProvider implements TimelineProviderInterface { $this->checkContext($context); - $metadataReport = $this->em->getClassMetadata('ChillReportBundle:Report'); - $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); - - return array( - 'id' => $metadataReport->getTableName() - .'.'.$metadataReport->getColumnName('id'), + $report = $this->em->getClassMetadata(Report::class); + [$where, $parameters] = $this->getWhereClause($context, $args); + + return TimelineSingleQuery::fromArray([ + 'id' => $report->getTableName() + .'.'.$report->getColumnName('id'), 'type' => 'report', - 'date' => $metadataReport->getTableName() - .'.'.$metadataReport->getColumnName('date'), - 'FROM' => $this->getFromClause($metadataReport, $metadataPerson), - 'WHERE' => $this->getWhereClause($metadataReport, $metadataPerson, - $args['person']) - ); + 'date' => $report->getTableName() + .'.'.$report->getColumnName('date'), + 'FROM' => $this->getFromClause($context), + 'WHERE' => $where, + 'parameters' => $parameters + ]); } - private function getWhereClause(ClassMetadata $metadataReport, - ClassMetadata $metadataPerson, Person $person) + private function getWhereClause(string $context, array $args): array { - $role = new Role('CHILL_REPORT_SEE'); - $reachableCenters = $this->helper->getReachableCenters($this->user, - $role); - $associationMapping = $metadataReport->getAssociationMapping('person'); - - // we start with reports having the person_id linked to person - // (currently only context "person" is supported) - $whereClause = sprintf('%s = %d', - $associationMapping['joinColumns'][0]['name'], - $person->getId()); - - // we add acl (reachable center and scopes) - $centerAndScopeLines = array(); - foreach ($reachableCenters as $center) { - $reachablesScopesId = array_map( - function(Scope $scope) { return $scope->getId(); }, - $this->helper->getReachableScopes($this->user, $role, - $person->getCenter()) - ); - - $centerAndScopeLines[] = sprintf('(%s = %d AND %s IN (%s))', - $metadataPerson->getTableName().'.'. - $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'], - $center->getId(), - $metadataReport->getTableName().'.'. - $metadataReport->getAssociationMapping('scope')['joinColumns'][0]['name'], - implode(',', $reachablesScopesId)); - + switch ($context) { + case 'person': + return $this->getWhereClauseForPerson($context, $args); + case 'center': + return $this->getWhereClauseForCenter($context, $args); + default: + throw new \UnexpectedValueException("This context $context is not implemented"); } - $whereClause .= ' AND ('.implode(' OR ', $centerAndScopeLines).')'; - - return $whereClause; + } + + private function getWhereClauseForCenter(string $context, array $args): array + { + $report = $this->em->getClassMetadata(Report::class); + $person = $this->em->getClassMetadata(Person::class); + $role = new Role('CHILL_REPORT_SEE'); + $reachableCenters = $this->helper->getReachableCenters($this->security->getUser(), + $role); + $reportPersonId = $report->getAssociationMapping('person')['joinColumns'][0]['name']; + $reportScopeId = $report->getAssociationMapping('scope')['joinColumns'][0]['name']; + $personCenterId = $person->getAssociationMapping('center')['joinColumns'][0]['name']; + // parameters for the query, will be filled later + $parameters = []; + + // the clause, that will be joined with an "OR" + $centerScopesClause = "({person}.{center_id} = ? ". + "AND {report}.{scopes_id} IN ({scopes_ids}))"; + // container for formatted clauses + $formattedClauses = []; + + $askedCenters = $args['centers']; + foreach ($reachableCenters as $center) { + if (FALSE === \in_array($center, $askedCenters)) { + continue; + } + + // add the center id to the parameters + $parameters[] = $center->getId(); + // loop over scopes + $scopeIds = []; + foreach ($this->helper->getReachableScopes($this->security->getUser(), + $role, $center) as $scope) { + if (\in_array($scope->getId(), $scopeIds)) { + continue; + } + $scopeIds[] = $scope->getId(); + } + + $formattedClauses[] = \strtr($centerScopesClause, [ + '{scopes_ids}' => \implode(', ', \array_fill(0, \count($scopeIds), '?')) + ]); + // append $scopeIds to parameters + $parameters = \array_merge($parameters, $scopeIds); + } + + if (0 === count($formattedClauses)) { + return [ 'FALSE = TRUE', [] ]; + } + + return [ + \strtr( + \implode(' OR ', $formattedClauses), + [ + '{person}' => $person->getTableName(), + '{center_id}' => $personCenterId, + '{report}' => $report->getTableName(), + '{scopes_id}' => $reportScopeId, + ] + ), + $parameters + ]; + } + + private function getWhereClauseForPerson(string $context, array $args): array + { + $report = $this->em->getClassMetadata(Report::class); + $person = $this->em->getClassMetadata(Person::class); + $role = new Role('CHILL_REPORT_SEE'); + $reportPersonId = $report->getAssociationMapping('person')['joinColumns'][0]['name']; + $reportScopeId = $report->getAssociationMapping('scope')['joinColumns'][0]['name']; + $personCenterId = $person->getAssociationMapping('center')['joinColumns'][0]['name']; + // parameters for the query, will be filled later + $parameters = [ $args['person']->getId() ]; + + // this is the final clause that we are going to fill + $clause = "{report}.{person_id} = ? AND {report}.{scopes_id} IN ({scopes_ids})"; + // iterate over reachable scopes + $scopes = $this->helper->getReachableScopes($this->security->getUser(), $role, + $args['person']->getCenter()); + + foreach ($scopes as $scope) { + if (\in_array($scope->getId(), $parameters)) { + continue; + } + + $parameters[] = $scope->getId(); + } + + if (1 === count($parameters)) { + // nothing change, we simplify the clause + $clause = "{report}.{person_id} = ? AND FALSE = TRUE"; + } + + return [ + \strtr( + $clause, + [ + '{report}' => $report->getTableName(), + '{person_id}' => $reportPersonId, + '{scopes_id}' => $reportScopeId, + '{scopes_ids}' => \implode(', ', + \array_fill(0, \count($parameters)-1, '?')) + ] + ), + $parameters + ]; } - private function getFromClause(ClassMetadata $metadataReport, - ClassMetadata $metadataPerson) + private function getFromClause(string $context): string { - $associationMapping = $metadataReport->getAssociationMapping('person'); - - return $metadataReport->getTableName().' JOIN ' - .$metadataPerson->getTableName().' ON ' - .$metadataPerson->getTableName().'.'. - $associationMapping['joinColumns'][0]['referencedColumnName'] - .' = ' - .$associationMapping['joinColumns'][0]['name'] + $report = $this->em->getClassMetadata(Report::class); + $person = $this->em->getClassMetadata(Person::class); + $reportPersonId = $report + ->getAssociationMapping('person')['joinColumns'][0]['name'] ; + $personId = $report + ->getAssociationMapping('person')['joinColumns'][0]['referencedColumnName'] + ; + + $clause = "{report} ". + "JOIN {person} ON {report}.{person_id} = {person}.{id_person} "; + + return \strtr($clause, + [ + '{report}' => $report->getTableName(), + '{person}' => $person->getTableName(), + '{person_id}' => $reportPersonId, + '{id_person}' => $personId + ] + ); } /** @@ -199,12 +254,11 @@ class TimelineReportProvider implements TimelineProviderInterface $this->checkContext($context); return array( - 'template' => 'ChillReportBundle:Timeline:report_person_context.html.twig', + 'template' => 'ChillReportBundle:Timeline:report.html.twig', 'template_data' => array( - 'report' => $entity, - 'custom_fields_in_summary' => $this->getFieldsToRender($entity, $context), - 'person' => $args['person'], - 'user' => $entity->getUser() + 'report' => $entity, + 'context' => $context, + 'custom_fields_in_summary' => $this->getFieldsToRender($entity, $context), ) ); } @@ -272,9 +326,9 @@ class TimelineReportProvider implements TimelineProviderInterface */ private function checkContext($context) { - if ($context !== 'person') { + if ($context !== 'person' && $context !== 'center') { throw new \LogicException("The context '$context' is not " - . "supported. Currently only 'person' is supported"); + . "supported. Currently only 'person' and 'center' is supported"); } } diff --git a/src/Bundle/ChillReportBundle/config/services.yaml b/src/Bundle/ChillReportBundle/config/services.yaml index fec944ff5..477a94746 100644 --- a/src/Bundle/ChillReportBundle/config/services.yaml +++ b/src/Bundle/ChillReportBundle/config/services.yaml @@ -20,11 +20,12 @@ services: arguments: - '@doctrine.orm.entity_manager' - '@chill.main.security.authorization.helper' - - '@security.token_storage' + - '@Symfony\Component\Security\Core\Security' - '@chill.custom_field.helper' - '%chill_custom_fields.show_empty_values%' tags: - { name: chill.timeline, context: 'person' } + - { name: chill.timeline, context: 'center' } chill.report.security.authorization.report_voter: class: Chill\ReportBundle\Security\Authorization\ReportVoter @@ -43,4 +44,4 @@ services: - "@doctrine.orm.entity_manager" tags: - { name: form.type, alias: chill_reportbundle_report } - \ No newline at end of file + diff --git a/src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition_person_context.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition.html.twig similarity index 68% rename from src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition_person_context.html.twig rename to src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition.html.twig index 234bbc134..fe65a1f1f 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition_person_context.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/Timeline/single_task_transition.html.twig @@ -7,6 +7,9 @@ {% else %} {{ '%user% has created the task'|trans({ '%user%': event.author.username }) }} {% endif %} + {% if 'person' != context %} + / {{ task.person|chill_entity_render_box({'addLink': true}) }} + {% endif %}
    @@ -29,5 +32,17 @@ {% endif %}
    +
diff --git a/src/Bundle/ChillTaskBundle/Timeline/SingleTaskTaskLifeCycleEventTimelineProvider.php b/src/Bundle/ChillTaskBundle/Timeline/SingleTaskTaskLifeCycleEventTimelineProvider.php index 6e40cb4a3..57a3832af 100644 --- a/src/Bundle/ChillTaskBundle/Timeline/SingleTaskTaskLifeCycleEventTimelineProvider.php +++ b/src/Bundle/ChillTaskBundle/Timeline/SingleTaskTaskLifeCycleEventTimelineProvider.php @@ -18,6 +18,7 @@ namespace Chill\TaskBundle\Timeline; use Chill\MainBundle\Timeline\TimelineProviderInterface; +use Chill\MainBundle\Timeline\TimelineSingleQuery; use Doctrine\ORM\EntityManagerInterface; use Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent; use Chill\TaskBundle\Entity\SingleTask; @@ -25,9 +26,8 @@ use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Workflow; /** + * Provide timeline elements related to tasks, in tasks context * - * - * @author Julien Fastré */ class SingleTaskTaskLifeCycleEventTimelineProvider implements TimelineProviderInterface { @@ -63,7 +63,7 @@ class SingleTaskTaskLifeCycleEventTimelineProvider implements TimelineProviderIn $singleTaskMetadata = $this->em ->getClassMetadata(SingleTask::class); - return [ + return TimelineSingleQuery::fromArray([ 'id' => sprintf('%s.%s.%s', $metadata->getSchemaName(), $metadata->getTableName(), $metadata->getColumnName('id')), 'type' => self::TYPE, 'date' => $metadata->getColumnName('datetime'), @@ -77,8 +77,9 @@ class SingleTaskTaskLifeCycleEventTimelineProvider implements TimelineProviderIn sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()), $singleTaskMetadata->getColumnName('id'), $args['task']->getId() - ) - ]; + ), + 'parameters' => [], + ]); } diff --git a/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php b/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php index db099a3e1..fe5a8f78d 100644 --- a/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php +++ b/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php @@ -21,43 +21,30 @@ use Chill\MainBundle\Timeline\TimelineProviderInterface; use Doctrine\ORM\EntityManagerInterface; use Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent; use Chill\TaskBundle\Entity\SingleTask; +use Chill\PersonBundle\Entity\Person; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Workflow; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Timeline\TimelineSingleQuery; /** - * + * Provide element for timeline for 'person' and 'center' context * - * @author Julien Fastré */ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface { - /** - * - * @var EntityManagerInterface - */ - protected $em; + protected EntityManagerInterface $em; - /** - * - * @var Registry - */ - protected $registry; + protected Registry $registry; - /** - * - * @var AuthorizationHelper - */ - protected $authorizationHelper; + protected AuthorizationHelper $authorizationHelper; + + protected Security $security; - /** - * - * @var TokenStorageInterface - */ - protected $tokenStorage; const TYPE = 'chill_task.transition'; @@ -65,60 +52,172 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface EntityManagerInterface $em, Registry $registry, AuthorizationHelper $authorizationHelper, - TokenStorageInterface $tokenStorage + Security $security ) { $this->em = $em; $this->registry = $registry; $this->authorizationHelper = $authorizationHelper; - $this->tokenStorage = $tokenStorage; + $this->security = $security; } public function fetchQuery($context, $args) { - if ($context !== 'person') { - throw new \LogicException(sprintf('%s is not able ' - . 'to render context %s', self::class, $context)); - } - $metadata = $this->em ->getClassMetadata(SingleTaskPlaceEvent::class); - $singleTaskMetadata = $this->em - ->getClassMetadata(SingleTask::class); - $user = $this->tokenStorage->getToken()->getUser(); - $circles = $this->authorizationHelper->getReachableCircles( - $user, new Role(ActivityVoter::SEE_DETAILS), $args['person']->getCenter()); - - if (count($circles) > 0) { - $circlesId = \array_map(function($c) { return $c->getId(); }, $circles); - $circleRestriction = sprintf('%s.%s.%s IN (%s)', - $singleTaskMetadata->getSchemaName(), // chill_task schema - $singleTaskMetadata->getTableName(), // single_task table name - $singleTaskMetadata->getAssociationMapping('circle')['joinColumns'][0]['name'], - \implode(', ', $circlesId) - ); - } else { - $circleRestriction = 'FALSE = TRUE'; + switch ($context) { + case 'person': + [ $where, $parameters ] = $this->getWhereClauseForPerson($args['person']); + break; + case 'center': + [ $where, $parameters ] = $this->getWhereClauseForCenter($args['centers']); + break; + default: + throw new \UnexpectedValueException("context {$context} is not supported"); } - - - return [ + + return TimelineSingleQuery::fromArray([ 'id' => sprintf('%s.%s.%s', $metadata->getSchemaName(), $metadata->getTableName(), $metadata->getColumnName('id')), 'type' => self::TYPE, 'date' => $metadata->getColumnName('datetime'), - 'FROM' => sprintf('%s JOIN %s ON %s = %s', - sprintf('%s.%s', $metadata->getSchemaName(), $metadata->getTableName()), - sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()), - $metadata->getAssociationMapping('task')['joinColumns'][0]['name'], - sprintf('%s.%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName(), $singleTaskMetadata->getColumnName('id')) - ), - 'WHERE' => sprintf('%s.%s = %d and %s', - sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()), - $singleTaskMetadata->getAssociationMapping('person')['joinColumns'][0]['name'], - $args['person']->getId(), - $circleRestriction - ) + 'FROM' => $this->getFromClause($context), + 'WHERE' => $where, + 'parameters' => $parameters + ]); + } + + private function getWhereClauseForCenter(array $centers): array + { + $taskEvent = $this->em->getClassMetadata(SingleTaskPlaceEvent::class); + $singleTask = $this->em->getClassMetadata(SingleTask::class); + $person = $this->em->getClassMetadata(Person::class); + $personFkCenter = $person->getAssociationMapping('center')['joinColumns'][0]['name']; + $taskFkCircle = $singleTask->getAssociationMapping('circle')['joinColumns'][0]['name']; + + // the parameters + $parameters = []; + + // the clause that we will repeat for each center, joined by 'OR' + $clause = "{person}.{center_id} = ? AND {task}.{circle} IN ({circle_ids})"; + + // array to gather clauses + $clauses = []; + + // loop over centers + foreach ($this->authorizationHelper->getReachableCenters( + $this->security->getUser(), new Role(ActivityVoter::SEE_DETAILS)) as $center) { + + if (FALSE === \in_array($center, $centers)) { + continue; + } + + // fill center parameter + $parameters[] = $center->getId(); + + // we loop over circles + $circles = $this->authorizationHelper->getReachableCircles( + $this->security->getUser(), new Role(ActivityVoter::SEE_DETAILS), $center); + $circleIds = []; + + foreach ($circles as $circle) { + $parameters[] = $circleIds[] = $circle->getId(); + } + + $clauses[] = \strtr( + $clause, + [ + '{person}' => $person->getTableName(), + '{center_id}' => $personFkCenter, + '{task}' => $singleTask->getSchemaName().".".$singleTask->getTableName(), + '{circle}' => $taskFkCircle, + '{circle_ids}' => \implode(', ', \array_fill(0, count($circleIds), '?')) + ] + ); + } + + if (0 === \count($clauses)) { + return [ 'FALSE = TRUE' , [] ]; + } + + return [ + \implode(' OR ', $clauses), + $parameters ]; + } + + private function getWhereClauseForPerson(Person $personArg): array + { + $taskEvent = $this->em->getClassMetadata(SingleTaskPlaceEvent::class); + $singleTask = $this->em->getClassMetadata(SingleTask::class); + $person = $this->em->getClassMetadata(Person::class); + $eventFkTask = $taskEvent->getAssociationMapping('task')['joinColumns'][0]['name']; + $taskFkPerson = $singleTask->getAssociationMapping('person')['joinColumns'][0]['name']; + $personPk = $singleTask->getAssociationMapping('person')['joinColumns'][0]['referencedColumnName']; + $taskFkCircle = $singleTask->getAssociationMapping('circle')['joinColumns'][0]['name']; + + + // the parameters + $parameters = []; + + // the clause that we will fill + $clause = "{person}.{person_id} = ? AND {task}.{circle} IN ({circle_ids})"; + + // person is the first parameter + $parameters[] = $personArg->getId(); + + // we loop over circles + $circles = $this->authorizationHelper->getReachableCircles( + $this->security->getUser(), new Role(ActivityVoter::SEE_DETAILS), $personArg->getCenter()); + + if (0 === count($circles)) { + // go fast to block access to every tasks + return [ "FALSE = TRUE", [] ]; + } + + foreach ($circles as $circle) { + $parameters[] = $circleIds[] = $circle->getId(); + } + + return [ + \strtr( + $clause, + [ + '{person}' => $person->getTableName(), + '{person_id}' => $person->getColumnName('id'), + '{task}' => $singleTask->getSchemaName().".".$singleTask->getTableName(), + '{circle}' => $taskFkCircle, + '{circle_ids}' => \implode(', ', \array_fill(0, count($circleIds), '?')) + ] + ), + $parameters + ]; + } + + private function getFromClause(string $context) + { + $taskEvent = $this->em->getClassMetadata(SingleTaskPlaceEvent::class); + $singleTask = $this->em->getClassMetadata(SingleTask::class); + $person = $this->em->getClassMetadata(Person::class); + $eventFkTask = $taskEvent->getAssociationMapping('task')['joinColumns'][0]['name']; + $taskFkPerson = $singleTask->getAssociationMapping('person')['joinColumns'][0]['name']; + $personPk = $singleTask->getAssociationMapping('person')['joinColumns'][0]['referencedColumnName']; + + $from = "{single_task_event} ". + "JOIN {single_task} ON {single_task}.{task_pk} = {single_task_event}.{event_fk_task} ". + "JOIN {person} ON {single_task}.{task_person_fk} = {person}.{person_pk}"; + + return \strtr( + $from, + [ + '{single_task}' => sprintf('%s.%s', $singleTask->getSchemaName(), $singleTask->getTableName()), + '{single_task_event}' => sprintf('%s.%s', $taskEvent->getSchemaName(), $taskEvent->getTableName()), + '{task_pk}' => $singleTask->getColumnName('id'), + '{event_fk_task}' => $eventFkTask, + '{person}' => $person->getTableName(), + '{task_person_fk}' => $taskFkPerson, + '{person_pk}' => $personPk + ] + ); } public function getEntities(array $ids) @@ -147,10 +246,11 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface $transition = $this->getTransitionByName($entity->getTransition(), $workflow); return [ - 'template' => 'ChillTaskBundle:Timeline:single_task_transition_person_context.html.twig', + 'template' => 'ChillTaskBundle:Timeline:single_task_transition.html.twig', 'template_data' => [ - 'person' => $args['person'], + 'context' => $context, 'event' => $entity, + 'task' => $entity->getTask(), 'transition' => $transition ] ]; diff --git a/src/Bundle/ChillTaskBundle/config/services/timeline.yaml b/src/Bundle/ChillTaskBundle/config/services/timeline.yaml index d6ca33606..04ef218b5 100644 --- a/src/Bundle/ChillTaskBundle/config/services/timeline.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/timeline.yaml @@ -4,10 +4,11 @@ services: $em: '@Doctrine\ORM\EntityManagerInterface' $registry: '@Symfony\Component\Workflow\Registry' $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' - $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' + $security: '@Symfony\Component\Security\Core\Security' public: true tags: - { name: 'chill.timeline', context: 'person' } + - { name: 'chill.timeline', context: 'center' } Chill\TaskBundle\Timeline\SingleTaskTaskLifeCycleEventTimelineProvider: arguments: