diff --git a/composer.json b/composer.json index 95074c18f..748a7d862 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "symfony/web-profiler-bundle": "^5.0", "symfony/var-dumper": "4.*", "symfony/debug-bundle": "^5.1", - "symfony/phpunit-bridge": "^5.2", + "symfony/phpunit-bridge": "^5.2", "nelmio/alice": "^3.8" }, "scripts": { 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/phpunit.xml.dist b/phpunit.xml.dist index 6fd80f9f1..25c5f0ff0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,12 +18,19 @@ src/Bundle/ChillMainBundle/Tests/ - src/Bundle/ChillPersonBundle/Tests/Export/* + + src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingPeriodControllerTest.php + + src/Bundle/ChillPersonBundle/Tests/Controller/PersonAddressControllerTest.php + + src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php + + src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php - --> diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index c0a25395c..b0a84fa4e 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -44,7 +44,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap; * Class Activity * * @package Chill\ActivityBundle\Entity - * @ORM\Entity() + * @ORM\Entity(repositoryClass="Chill\ActivityBundle\Repository\ActivityRepository") * @ORM\Table(name="activity") * @ORM\HasLifecycleCallbacks() * @DiscriminatorMap(typeProperty="type", mapping={ 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/AddressController.php b/src/Bundle/ChillMainBundle/Controller/AddressController.php deleted file mode 100644 index 1aeb39062..000000000 --- a/src/Bundle/ChillMainBundle/Controller/AddressController.php +++ /dev/null @@ -1,63 +0,0 @@ -json($address); - default: - throw new BadRequestException('Unsupported format'); - } - } - - - /** - * Get API Data for showing endpoint - * - * @Route( - * "/{_locale}/main/api/1.0/address-reference/{address_reference_id}/show.{_format}", - * name="chill_main_address_reference_api_show" - * ) - * @ParamConverter("addressReference", options={"id": "address_reference_id"}) - */ - public function showAddressReference(AddressReference $addressReference, $_format): Response - { - // TODO check ACL ? - switch ($_format) { - case 'json': - return $this->json($addressReference); - default: - throw new BadRequestException('Unsupported format'); - } - - } -} diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php new file mode 100644 index 000000000..cbe0ea0a5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php @@ -0,0 +1,27 @@ +query->has('postal_code')) { + + $qb->where('e.postcode = :postal_code') + ->setParameter('postal_code', $request->query->get('postal_code')); + + } + } + +} diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php new file mode 100644 index 000000000..026784208 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php @@ -0,0 +1,27 @@ +query->has('country')) { + + $qb->where('e.country = :country') + ->setParameter('country', $request->query->get('country')); + + } + } + +} 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/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 5644138e6..f6d10ec9e 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -284,6 +284,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, ] ], [ + 'controller' => \Chill\MainBundle\Controller\AddressReferenceAPIController::class, 'class' => \Chill\MainBundle\Entity\AddressReference::class, 'name' => 'address_reference', 'base_path' => '/api/1.0/main/address-reference', @@ -304,6 +305,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, ] ], [ + 'controller' => \Chill\MainBundle\Controller\PostalCodeAPIController::class, 'class' => \Chill\MainBundle\Entity\PostalCode::class, 'name' => 'postal_code', 'base_path' => '/api/1.0/main/postal-code', diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index 43c21ae59..2e6f83a69 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -9,11 +9,11 @@ use \JsonSerializable; * */ class Point implements JsonSerializable { - private float $lat; - private float $lon; + private ?float $lat = null; + private ?float $lon = null; public static string $SRID = '4326'; - private function __construct(float $lon, float $lat) + private function __construct(?float $lon, ?float $lat) { $this->lat = $lat; $this->lon = $lon; diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 2003a7910..2fc024e8c 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -4,6 +4,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Serializer\Annotation\Groups; use Chill\MainBundle\Doctrine\Model\Point; use Chill\ThirdPartyBundle\Entity\ThirdParty; @@ -22,6 +23,7 @@ class Address * @ORM\Id * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") + * @groups({"write"}) */ private $id; @@ -29,6 +31,7 @@ class Address * @var string * * @ORM\Column(type="string", length=255) + * @groups({"write"}) */ private $street = ''; @@ -36,6 +39,7 @@ class Address * @var string * * @ORM\Column(type="string", length=255) + * @groups({"write"}) */ private $streetNumber = ''; @@ -43,6 +47,7 @@ class Address * @var PostalCode * * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") + * @groups({"write"}) */ private $postcode; @@ -50,6 +55,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) + * @groups({"write"}) */ private $floor; @@ -57,6 +63,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) + * @groups({"write"}) */ private $corridor; @@ -64,6 +71,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) + * @groups({"write"}) */ private $steps; @@ -71,6 +79,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"write"}) */ private $buildingName; @@ -78,6 +87,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) + * @groups({"write"}) */ private $flat; @@ -85,6 +95,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"write"}) */ private $distribution; @@ -92,6 +103,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"write"}) */ private $extra; @@ -102,6 +114,7 @@ class Address * @var \DateTime * * @ORM\Column(type="date") + * @groups({"write"}) */ private $validFrom; @@ -112,11 +125,13 @@ class Address * @var \DateTime|null * * @ORM\Column(type="date", nullable=true) + * @groups({"write"}) */ private $validTo; /** * True if the address is a "no address", aka homeless person, ... + * @groups({"write"}) * * @var bool */ @@ -128,6 +143,7 @@ class Address * @var Point|null * * @ORM\Column(type="point", nullable=true) + * @groups({"write"}) */ private $point; @@ -137,6 +153,7 @@ class Address * @var ThirdParty|null * * @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") + * @groups({"write"}) * @ORM\JoinColumn(nullable=true, onDelete="SET NULL") */ private $linkedToThirdParty; diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index b1fca205d..d13de4de9 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -4,6 +4,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Chill\MainBundle\Doctrine\Model\Point; +use Symfony\Component\Serializer\Annotation\Groups; /** * @ORM\Entity() @@ -16,21 +17,25 @@ class AddressReference * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") + * @groups({"read"}) */ private $id; /** * @ORM\Column(type="string", length=255) + * @groups({"read"}) */ private $refId; /** * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"read"}) */ private $street; /** * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"read"}) */ private $streetNumber; @@ -38,16 +43,19 @@ class AddressReference * @var PostalCode * * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") + * @groups({"read"}) */ private $postcode; /** * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"read"}) */ private $municipalityCode; /** * @ORM\Column(type="string", length=255, nullable=true) + * @groups({"read"}) */ private $source; @@ -57,6 +65,7 @@ class AddressReference * @var Point * * @ORM\Column(type="point") + * @groups({"read"}) */ private $point; diff --git a/src/Bundle/ChillMainBundle/Entity/Country.php b/src/Bundle/ChillMainBundle/Entity/Country.php index f8f68c3f3..932944d03 100644 --- a/src/Bundle/ChillMainBundle/Entity/Country.php +++ b/src/Bundle/ChillMainBundle/Entity/Country.php @@ -3,6 +3,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; /** * Country @@ -20,6 +21,7 @@ class Country * @ORM\Id * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") + * @groups({"read"}) */ private $id; @@ -27,13 +29,16 @@ class Country * @var string * * @ORM\Column(type="json_array") + * @groups({"read"}) + * */ private $name; - + /** * @var string * * @ORM\Column(type="string", length=3) + * @groups({"read"}) */ private $countryCode; @@ -41,7 +46,7 @@ class Country /** * Get id * - * @return integer + * @return integer */ public function getId() { @@ -57,20 +62,20 @@ class Country public function setName($name) { $this->name = $name; - + return $this; } /** * Get name * - * @return string + * @return string */ public function getName() { return $this->name; } - + /** * @return string */ @@ -90,12 +95,12 @@ class Country /** * - * @param string $countryCode + * @param string $countryCode */ public function setCountryCode($countryCode) { $this->countryCode = $countryCode; return $this; } - + } diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index 3cff0a6bf..a70a955b8 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -3,6 +3,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; /** * PostalCode @@ -25,6 +26,7 @@ class PostalCode * @ORM\Id * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") + * @groups({"read"}) */ private $id; @@ -32,6 +34,7 @@ class PostalCode * @var string * * @ORM\Column(type="string", length=255, name="label") + * @groups({"read"}) */ private $name; @@ -39,6 +42,7 @@ class PostalCode * @var string * * @ORM\Column(type="string", length=100) + * @groups({"read"}) */ private $code; @@ -46,6 +50,7 @@ class PostalCode * @var Country * * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country") + * @groups({"read"}) */ private $country; diff --git a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss index 8a77ac48f..cc9c090e2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss @@ -23,7 +23,7 @@ // @import "bootstrap/scss/button-group"; // @import "bootstrap/scss/input-group"; // @import "bootstrap/scss/custom-forms"; -// @import "bootstrap/scss/nav"; +@import "bootstrap/scss/nav"; // @import "bootstrap/scss/navbar"; // @import "bootstrap/scss/card"; // @import "bootstrap/scss/breadcrumb"; diff --git a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss index 77aafbc3d..0877901f3 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss @@ -56,7 +56,7 @@ div#header-accompanying_course-name { background: none repeat scroll 0 0 #718596; color: #FFF; h1 { - margin: 0.4em 0; + margin: 0.4em 0; } span { a { @@ -77,7 +77,7 @@ div#header-accompanying_course-details { /* * FLEX RESPONSIVE TABLE/BLOCK PRESENTATION */ -div.flex-bloc, +div.flex-bloc, div.flex-table { h2, h3, h4, dl, p { margin: 0; @@ -103,25 +103,35 @@ div.flex-table { * Bloc appearance */ div.flex-bloc { - box-sizing: border-box; + box-sizing: border-box; display: flex; flex-direction: row; flex-wrap: wrap; align-items: stretch; align-content: stretch; - + div.item-bloc { flex-grow: 0; flex-shrink: 1; flex-basis: 50%; margin: 0; padding: 1em; + border-top: 0; + &:nth-child(1), &:nth-child(2) { + border-top: 1px solid #000; + } + border-left: 0; + &:nth-child(odd) { + border-left: 1px solid #000; + } + + //background-color: #e6e6e6; display: flex; flex-direction: column; - + div.item-row { flex-grow: 1; flex-shrink: 1; flex-basis: auto; display: flex; flex-direction: column; - + div.item-col { &:first-child { flex-grow: 0; flex-shrink: 0; flex-basis: auto; @@ -129,7 +139,7 @@ div.flex-bloc { &:last-child { flex-grow: 1; flex-shrink: 1; flex-basis: auto; display: flex; - + .list-content { // ul, dl, or div } ul.record_actions { @@ -139,7 +149,7 @@ div.flex-bloc { li { margin-right: 5px; } - } + } } } } @@ -150,24 +160,30 @@ div.flex-bloc { @media only screen and (max-width: 900px) { flex-direction: column; margin: auto 0; + div.item-bloc { + border-left: 1px solid #000; + &:nth-child(2) { + border-top: 0; + } + } } } /* * Table appearance -*/ +*/ div.flex-table { display: flex; flex-direction: column; align-items: stretch; align-content: stretch; - + div.item-bloc { display: flex; flex-direction: column; padding: 1em; &:nth-child(even) { - background-color: #e6e6e6; + background-color: #e6e6e6; } div.item-row { @@ -179,7 +195,7 @@ div.flex-table { padding-top: 0.5em; flex-direction: column; } - + div.item-col { &:first-child { flex-grow: 0; flex-shrink: 0; flex-basis: 33%; @@ -188,7 +204,7 @@ div.flex-table { flex-grow: 1; flex-shrink: 1; flex-basis: auto; display: flex; justify-content: flex-end; - + .list-content { // ul, dl, or div } ul.record_actions { @@ -198,7 +214,7 @@ div.flex-table { li { margin-right: 5px; } - } + } } } @media only screen and (max-width: 900px) { @@ -211,9 +227,47 @@ div.flex-table { } } } - - // neutralize + + // neutralize div.chill_address div.chill_address_address p { text-indent: 0; } } } -} +} + + + +/* +* Address form +*/ +div.address_form { + display: flex; + flex-direction: column; + div.address_form__header { + + } + div.address_form__select { + display: flex; + flex-direction: row; + justify-content: space-between; + + div.address_form__select__body { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + div.address_form__select__map { + margin: 0px 20px; + div#address_map { + height:400px; + width:400px; + } + } + } + div.address_form__more { + + } +} + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue index 461b0038e..7c27db341 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue @@ -1,5 +1,5 @@