From c3ef8d112c33285f825f6f433d514dad376c8119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 May 2021 16:25:56 +0200 Subject: [PATCH] first impl for global timeline: apply on activities --- .../ChillActivityBundle/Entity/Activity.php | 2 +- .../Repository/ActivityACLAwareRepository.php | 246 ++++++++++++++++++ .../Repository/ActivityRepository.php | 42 +++ .../activity_person_context.html.twig | 8 +- .../Timeline/TimelineActivityProvider.php | 85 ++++-- .../ChillActivityBundle/config/services.yaml | 2 + .../config/services/repositories.yaml | 18 +- .../Controller/TimelineCenterController.php | 91 +++++++ .../views/Timeline/chain_timelines.html.twig | 7 + .../Resources/views/Timeline/index.html.twig | 22 +- .../Timeline/TimelineBuilder.php | 88 ++++--- src/Bundle/ChillMainBundle/config/routes.yaml | 4 + .../config/services/timeline.yaml | 5 +- .../Controller/TimelinePersonController.php | 29 +-- .../Resources/views/Entity/person.html.twig | 18 ++ .../Templating/Entity/PersonRender.php | 33 +-- .../config/services/controller.yaml | 1 + .../config/services/templating.yaml | 1 + 18 files changed, 604 insertions(+), 98 deletions(-) create mode 100644 src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php create mode 100644 src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php create mode 100644 src/Bundle/ChillMainBundle/Controller/TimelineCenterController.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Timeline/chain_timelines.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig 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..b8684a0a5 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -0,0 +1,246 @@ +, + * + * 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 = $this->getWhereClause($context, $args); + + return [ + 'id' => $metadataActivity->getTableName() + .'.'.$metadataActivity->getColumnName('id'), + 'type' => 'activity', + 'date' => $metadataActivity->getTableName() + .'.'.$metadataActivity->getColumnName('date'), + 'FROM' => $from, + 'WHERE' => $where + ]; + } + + 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 = []; + + // condition will be: + // FROM activity JOIN person -- not set by us + // ON activity.person_id = person.id -- not set by us + // WHERE -- not set by us + // activity.person_id = ? AND -- only if $context = person + // ( -- begin loop through centers, center#0 + // person.center_id = ? + // AND ( -- begin loop for scopes within centers + // activity.scope_id = ? -- scope#0 + // OR -- if scope#i where i > 0 + // activity.scope_id = ? -- scope#1 + // ) + // ) + // OR -- if center#i where i > 0 + // ( -- begin loop through centers, center#1 + // person.center_id = ? + // AND ( -- begin loop for scopes within centers + // activity.scope_id = ? -- scope#0 + // OR -- if scope#i where i > 0 + // activity.scope_id = ? -- scope#1 + // ) + // ) + + $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]; + } + +} +/* + $qb = $this->repository->createQueryBuilder('a'); + $qb->select(['a.id', "'activity'", 'a.date']); + $qb->join('a.person', 'p'); + + switch($context) { + case 'center': + $qb->where($this->queryTimelineIndexerWhereForCenter($qb, $args['centers'])); + break; + default: + throw new \LogicException('context not supported'); + } + + if ($from) { + $qb->andWhere($qb->gt('a.date', ':from')); + $qb->setParameter('from', $from); + } + + if ($to) { + $qb->andWhere($qb->gt('a.date', ':to')); + $qb->setParameter('to', $to); + } + + return $qb->getQuery(); + } + + private function queryTimelineIndexerWhereForCenter(QueryBuilder $qb, array $centers): Orx + { + $i = 0; + $orx = $qb->expr()->orX(); + + foreach ($centers as $center) { + $andx = $qb->expr()->andX(); + $andx->add($qb->expr()->eq('p.center', ":center_$i")); + $qb->setParameter("center_$i", $center); + $i++; + + $scopes = $this->authorizationHelper->getReachableCircles( + $this->tokenStorage->getToken()->getUser(), + new Role(ActivityVoter::SEE_DETAILS), + $center, + ); + + foreach ($scopes as $scope) { + $andx->add($qb->expr()->eq('a.scope', ":scope_$i")); + $qb->setParameter("scope_$i", $scope); + $i++; + } + + $orx->add($andx); + } + + return $orx; + } +} */ 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..ce9a68ea4 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 }}

+

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

{{ '%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..52e1027a8 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; @@ -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,10 +93,13 @@ class TimelineActivityProvider implements TimelineProviderInterface */ public function fetchQuery($context, array $args) { - $this->checkContext($context); + //$this->checkContext($context); + // + if ('center' === $context) { + return $this->aclAwareRepository->queryTimelineIndexer($context, $args); + } $metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity'); - $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); return array( 'id' => $metadataActivity->getTableName() @@ -102,10 +112,40 @@ class TimelineActivityProvider implements TimelineProviderInterface $args['person']) ); } - - private function getWhereClause(ClassMetadata $metadataActivity, - ClassMetadata $metadataPerson, Person $person) + + private function getFromClause(string $context) { + switch ($context) { + case 'person': + return $this->getFromClausePerson($metadataActivity, $metadataPerson); + } + } + + private function getWhereClause(string $context, array $args) + { + switch ($context) { + case 'person': + return $this->getWhereClause($args['person']); + } + } + + /** + * + * @var $centers array|Center[] + */ + private function getWhereClauseForCenter(array $centers) + { + $clause = ""; + $role = new Role('CHILL_ACTIVITY_SEE'); + + + } + + private function getWhereClauseForPerson(Person $person) + { + $metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity'); + $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); + $role = new Role('CHILL_ACTIVITY_SEE'); $reachableCenters = $this->helper->getReachableCenters($this->user, $role); @@ -144,9 +184,25 @@ class TimelineActivityProvider implements TimelineProviderInterface return $whereClause; } - private function getFromClause(ClassMetadata $metadataActivity, - ClassMetadata $metadataPerson) + private function getFromClausePerson() { + $metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity'); + $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); + $associationMapping = $metadataActivity->getAssociationMapping('person'); + + return $metadataActivity->getTableName().' JOIN ' + .$metadataPerson->getTableName().' ON ' + .$metadataPerson->getTableName().'.'. + $associationMapping['joinColumns'][0]['referencedColumnName'] + .' = ' + .$associationMapping['joinColumns'][0]['name'] + ; + } + + private function getFromClauseCenter() + { + $metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity'); + $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); $associationMapping = $metadataActivity->getAssociationMapping('person'); return $metadataActivity->getTableName().' JOIN ' @@ -183,14 +239,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 +265,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 9ef22f43f..b4b1f8274 100644 --- a/src/Bundle/ChillActivityBundle/config/services.yaml +++ b/src/Bundle/ChillActivityBundle/config/services.yaml @@ -22,6 +22,8 @@ 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' } 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/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/Timeline/TimelineBuilder.php b/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php index 6d061d0a2..2e4102af6 100644 --- a/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php +++ b/src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php @@ -23,6 +23,8 @@ 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 @@ -78,14 +80,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 +102,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 +158,56 @@ 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 */ - private function buildSelectQuery(TimelineProviderInterface $provider, $context, array $args) + private function buildSelectQuery(array $data): array { - $data = $provider->fetchQuery($context, $args); - - return sprintf( + $parameters = []; + + $sql = sprintf( 'SELECT %s AS id, ' . '%s AS "date", ' . "'%s' AS type " @@ -197,16 +217,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 +237,8 @@ class TimelineBuilder implements ContainerAwareInterface ->addScalarResult('date', 'date'); return $this->em->createNativeQuery($query, $resultSetMapping) - ->getArrayResult(); + ->setParameters($parameters) + ->getArrayResult(); } /** @@ -274,7 +298,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/config/routes.yaml b/src/Bundle/ChillMainBundle/config/routes.yaml index 3fd7eafab..33b4e67de 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/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/Resources/views/Entity/person.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig new file mode 100644 index 000000000..54482ee29 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig @@ -0,0 +1,18 @@ + + {% if addLink and is_granted('CHILL_PERSON_SEE', person) %} + {% set showLink = true %} + + {% endif %} + {{ person.firstName }} + {{ person.lastName }} + {% if addAltNames %} + {% for n in person.altNames %} + {% if loop.first %}({% else %} {% endif %} + + {{ n.label }} + + {% if loop.last %}){% endif %} + {% endfor %} + {% endif %} + {% if showLink is defined %}{% endif %} + 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/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index dc575f320..d36519765 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 3456c96de..338248540 100644 --- a/src/Bundle/ChillPersonBundle/config/services/templating.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/templating.yaml @@ -2,6 +2,7 @@ services: Chill\PersonBundle\Templating\Entity\PersonRender: arguments: $configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' + $engine: '@Symfony\Component\Templating\EngineInterface' tags: - 'chill.render_entity'