mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Merge branch 'master' into 38_modal_on-the-fly
This commit is contained in:
		| @@ -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": { | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|      * @author Champs Libres <info@champs-libres.coop> | ||||
|      */ | ||||
|     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() ] | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|         //.... | ||||
|   | ||||
| @@ -18,12 +18,19 @@ | ||||
|        <testsuite name="MainBundle"> | ||||
|          <directory suffix="Test.php">src/Bundle/ChillMainBundle/Tests/</directory> | ||||
|        </testsuite> | ||||
|        <!-- remove tests for person temporarily  | ||||
|        <testsuite name="PersonBundle"> | ||||
|          <directory suffix="Test.php">src/Bundle/ChillPersonBundle/Tests/</directory> | ||||
|          <!-- test for export will be runned later --> | ||||
|          <exclude>src/Bundle/ChillPersonBundle/Tests/Export/*</exclude> | ||||
|          <!-- we are rewriting accompanying periods... Work in progress --> | ||||
|          <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingPeriodControllerTest.php</exclude> | ||||
|          <!-- we are rewriting address, Work in progress --> | ||||
|          <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonAddressControllerTest.php</exclude> | ||||
|          <!-- find a solution to create multiple configs --> | ||||
|          <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php</exclude> | ||||
|          <!-- temporarily removed, the time to find a fix --> | ||||
|          <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> | ||||
|         </testsuite> | ||||
|         --> | ||||
|     </testsuites> | ||||
|  | ||||
|     <listeners> | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -0,0 +1,169 @@ | ||||
| <?php | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * Copyright (C) 2021, Champs Libres Cooperative SCRLFS, | ||||
|  * <http://www.champs-libres.coop>, <info@champs-libres.coop> | ||||
|  * | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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]; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,42 @@ | ||||
| <?php | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * Copyright (C) 2021, Champs Libres Cooperative SCRLFS, | ||||
|  * <http://www.champs-libres.coop>, <info@champs-libres.coop> | ||||
|  * | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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); | ||||
|     } | ||||
|      | ||||
| } | ||||
| @@ -1,11 +1,11 @@ | ||||
| {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %} | ||||
|  | ||||
| <div> | ||||
|     <h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span></h3> | ||||
|   <h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span>{% if 'person' != context %} / {{ activity.person|chill_entity_render_box({'addLink': true}) }}{% endif %}</h3> | ||||
|     <div class="statement"> | ||||
|         <span class="statement">{{ '%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') } | ||||
|         ) }}</span> | ||||
| @@ -29,13 +29,13 @@ | ||||
|  | ||||
|     <ul class="record_actions"> | ||||
|         <li> | ||||
|             <a href="{{ path('chill_activity_activity_show', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-view"> | ||||
|             <a href="{{ path('chill_activity_activity_show', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-view"> | ||||
|                 {{ 'Show the activity'|trans }} | ||||
|             </a> | ||||
|         </li> | ||||
|         {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} | ||||
|         <li> | ||||
|             <a href="{{ path('chill_activity_activity_edit', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-edit"> | ||||
|             <a href="{{ path('chill_activity_activity_edit', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-edit"> | ||||
|                 {{ 'Edit the activity'|trans }} | ||||
|             </a> | ||||
|         </li> | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  * @author Champs Libres <info@champs-libres.coop> | ||||
|  */ | ||||
| */ | ||||
| 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"); | ||||
|         } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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' | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|         ); | ||||
|     } | ||||
|      | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,63 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpFoundation\Exception\BadRequestException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Chill\MainBundle\Entity\Address; | ||||
| use Chill\MainBundle\Entity\AddressReference; | ||||
|  | ||||
| /** | ||||
|  * Class AddressController | ||||
|  * | ||||
|  * @package Chill\MainBundle\Controller | ||||
|  */ | ||||
| class AddressController extends AbstractController | ||||
| { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get API Data for showing endpoint | ||||
|      * | ||||
|      * @Route( | ||||
|      *     "/{_locale}/main/api/1.0/address/{address_id}/show.{_format}", | ||||
|      *     name="chill_main_address_api_show" | ||||
|      * ) | ||||
|      * @ParamConverter("address", options={"id": "address_id"}) | ||||
|      */ | ||||
|     public function showAddress(Address $address, $_format): Response | ||||
|     { | ||||
|         // TODO check ACL ? | ||||
|         switch ($_format) { | ||||
|             case 'json': | ||||
|                 return $this->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'); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\CRUD\Controller\ApiController; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| /** | ||||
|  * Class AddressReferenceAPIController | ||||
|  * | ||||
|  * @package Chill\MainBundle\Controller | ||||
|  * @author Champs Libres | ||||
|  */ | ||||
| class AddressReferenceAPIController extends ApiController | ||||
| { | ||||
|  | ||||
|     protected function customizeQuery(string $action, Request $request, $qb): void | ||||
|     { | ||||
|         if ($request->query->has('postal_code')) { | ||||
|  | ||||
|             $qb->where('e.postcode = :postal_code') | ||||
|                ->setParameter('postal_code', $request->query->get('postal_code')); | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\CRUD\Controller\ApiController; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| /** | ||||
|  * Class PostalCodeAPIController | ||||
|  * | ||||
|  * @package Chill\MainBundle\Controller | ||||
|  * @author Champs Libres | ||||
|  */ | ||||
| class PostalCodeAPIController extends ApiController | ||||
| { | ||||
|  | ||||
|     protected function customizeQuery(string $action, Request $request, $qb): void | ||||
|     { | ||||
|         if ($request->query->has('country')) { | ||||
|  | ||||
|             $qb->where('e.country = :country') | ||||
|                ->setParameter('country', $request->query->get('country')); | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,91 @@ | ||||
| <?php | ||||
|  | ||||
| /* | ||||
|  * Copyright (C) 2015 Champs-Libres Coopérative <info@champs-libres.coop> | ||||
|  * | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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 | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -50,7 +50,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 { | ||||
| @@ -71,7 +71,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; | ||||
| @@ -85,38 +85,38 @@ 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; | ||||
|       border: 1px solid #000; | ||||
|       padding: 1em; | ||||
|        | ||||
|  | ||||
|       border-top: 0; | ||||
|       &:nth-child(1), &:nth-child(2) {  | ||||
|       &: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; | ||||
| @@ -124,7 +124,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 { | ||||
| @@ -134,7 +134,7 @@ div.flex-bloc { | ||||
|                   li { | ||||
|                      margin-right: 5px; | ||||
|                   } | ||||
|                }             | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|       } | ||||
| @@ -147,7 +147,7 @@ div.flex-bloc { | ||||
|       margin: auto 0; | ||||
|       div.item-bloc { | ||||
|          border-left: 1px solid #000; | ||||
|          &:nth-child(2) {  | ||||
|          &:nth-child(2) { | ||||
|             border-top: 0; | ||||
|          } | ||||
|       } | ||||
| @@ -156,13 +156,13 @@ div.flex-bloc { | ||||
|  | ||||
| /* | ||||
| * Table appearance | ||||
| */  | ||||
| */ | ||||
| div.flex-table { | ||||
|    display: flex; | ||||
|    flex-direction: column; | ||||
|    align-items: stretch; | ||||
|    align-content: stretch; | ||||
|     | ||||
|  | ||||
|    div.item-bloc { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
| @@ -173,7 +173,7 @@ div.flex-table { | ||||
|          border-top: 1px solid #000; | ||||
|       } | ||||
|       &:nth-child(even) { | ||||
|          background-color: #e6e6e6;          | ||||
|          background-color: #e6e6e6; | ||||
|       } | ||||
|  | ||||
|       div.item-row { | ||||
| @@ -185,7 +185,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%; | ||||
| @@ -194,7 +194,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 { | ||||
| @@ -204,7 +204,7 @@ div.flex-table { | ||||
|                   li { | ||||
|                      margin-right: 5px; | ||||
|                   } | ||||
|                }             | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|          @media only screen and (max-width: 900px) { | ||||
| @@ -217,9 +217,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 { | ||||
|  | ||||
|    } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|     | ||||
|  | ||||
|    <div v-if="address.address"> | ||||
|       {{ address.address.street }}, {{ address.address.streetNumber }} | ||||
|    </div> | ||||
| @@ -9,7 +9,7 @@ | ||||
|    <div v-if="address.country"> | ||||
|       {{ address.country.name }} | ||||
|    </div> | ||||
|     | ||||
|  | ||||
|    <add-address | ||||
|       @addNewAddress="addNewAddress"> | ||||
|    </add-address> | ||||
| @@ -33,7 +33,26 @@ export default { | ||||
|    methods: { | ||||
|       addNewAddress({ address, modal }) { | ||||
|          console.log('@@@ CLICK button addNewAdress', address); | ||||
|          this.$store.dispatch('addAddress', address.selected); | ||||
|  | ||||
|          let newAddress = { | ||||
|             'isNoAddress': address.isNoAddress, | ||||
|             'street': address.selected.address.street, | ||||
|             'streetNumber': address.selected.address.streetNumber, | ||||
|             'postcode': {'id': address.selected.city.id }, | ||||
|             'floor': address.floor, | ||||
|             'corridor': address.corridor, | ||||
|             'steps': address.steps, | ||||
|             'flat': address.flat, | ||||
|             'buildingName': address.buildingName, | ||||
|             'distribution': address.distribution, | ||||
|             'extra': address.extra | ||||
|          }; | ||||
|  | ||||
|          if (address.selected.address.point !== undefined){ | ||||
|             newAddress = Object.assign(newAddress, {'point': address.selected.address.point.coordinates}); | ||||
|          } | ||||
|  | ||||
|          this.$store.dispatch('addAddress', newAddress); | ||||
|          modal.showModal = false; | ||||
|       } | ||||
|    } | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| const addressMessages = { | ||||
|    fr: { | ||||
|       add_an_address: 'Ajouter une adresse', | ||||
|       select_an_address: 'Sélectionner une adresse', | ||||
|       add_an_address_title: 'Ajouter une adresse', | ||||
|       select_an_address_title: 'Sélectionner une adresse', | ||||
|       fill_an_address: 'Compléter l\'adresse', | ||||
|       select_country: 'Choisir le pays', | ||||
|       select_city: 'Choisir une localité', | ||||
|       select_address: 'Choisir une adresse', | ||||
|       isNoAddress: 'L\'adresse n\'est pas celle d\'un domicile fixe ?', | ||||
|       create_address: 'Appuyer sur "Entrée" pour créer une nouvelle adresse', | ||||
|       isNoAddress: 'Pas d\'adresse complète', | ||||
|       floor: 'Étage', | ||||
|       corridor: 'Couloir', | ||||
|       steps: 'Escalier', | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import 'es6-promise/auto'; | ||||
| import { createStore } from 'vuex'; | ||||
|  | ||||
| // le fetch POST serait rangé dans la logique du composant qui appelle AddAddress | ||||
| //import { postAddress } from '... api'  | ||||
| import { postAddress } from '../../_api/AddAddress' | ||||
|  | ||||
| const debug = process.env.NODE_ENV !== 'production'; | ||||
|  | ||||
| @@ -10,11 +9,14 @@ const store = createStore({ | ||||
|    strict: debug, | ||||
|    state: { | ||||
|       address: {}, | ||||
|       errorMsg: {} | ||||
|       errorMsg: [] | ||||
|    }, | ||||
|    getters: { | ||||
|    }, | ||||
|    mutations: { | ||||
|       catchError(state, error) { | ||||
|          state.errorMsg.push(error); | ||||
|       }, | ||||
|       addAddress(state, address) { | ||||
|          console.log('@M addAddress address', address); | ||||
|          state.address = address; | ||||
| @@ -23,19 +25,19 @@ const store = createStore({ | ||||
|    actions: { | ||||
|       addAddress({ commit }, payload) { | ||||
|          console.log('@A addAddress payload', payload); | ||||
|          commit('addAddress', payload); // à remplacer par  | ||||
|           | ||||
|          // fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok.  | ||||
|          // La confirmation est l'adresse elle-même. | ||||
|          // | ||||
|          //   postAddress(payload)  | ||||
|          //   .fetch(address => new Promise((resolve, reject) => { | ||||
|          //      commit('addAddress', address); | ||||
|          //      resolve(); | ||||
|          //   })) | ||||
|          //   .catch((error) => {  | ||||
|          //      state.errorMsg.push(error.message);  | ||||
|          //   });  | ||||
|          //commit('addAddress', payload); // à remplacer par la suite | ||||
|  | ||||
|          //fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok. | ||||
|          //La confirmation est l'adresse elle-même. | ||||
|  | ||||
|          postAddress(payload) | ||||
|             .then(address => new Promise((resolve, reject) => { | ||||
|                commit('addAddress', address); | ||||
|                resolve(); | ||||
|             })) | ||||
|             .catch((error) => { | ||||
|                commit('catchError', error); | ||||
|             }); | ||||
|       } | ||||
|    } | ||||
| }); | ||||
|   | ||||
| @@ -1,37 +1,43 @@ | ||||
| /* | ||||
| * Endpoint countries GET | ||||
| * TODO | ||||
| * Endpoint chill_api_single_country__index | ||||
| * method GET, get Country Object | ||||
| * @returns {Promise} a promise containing all Country object | ||||
| */ | ||||
| const fetchCountries = () => { | ||||
|    console.log('<<< fetching countries'); | ||||
|    return [ | ||||
|       {id: 1, name: 'France', countryCode: 'FR'}, | ||||
|       {id: 2, name: 'Belgium', countryCode: 'BE'} | ||||
|    ]; | ||||
|  | ||||
|    const url = `/api/1.0/main/country.json?item_per_page=1000`; | ||||
|    return fetch(url) | ||||
|       .then(response => { | ||||
|          if (response.ok) { return response.json(); } | ||||
|          throw Error('Error with request resource response'); | ||||
|       }); | ||||
| }; | ||||
|  | ||||
| /* | ||||
| * Endpoint cities GET  | ||||
| * TODO | ||||
| * Endpoint chill_api_single_postal_code__index | ||||
| * method GET, get Country Object | ||||
| * @returns {Promise} a promise containing all Postal Code objects filtered with country | ||||
| */ | ||||
| const fetchCities = (country) => {    | ||||
| const fetchCities = (country) => { | ||||
|    console.log('<<< fetching cities for', country); | ||||
|    return [ | ||||
|       {id: 1, name: 'Bruxelles', code: '1000', country: 'BE'}, | ||||
|       {id: 2, name: 'Aisne', code: '85045', country: 'FR'}, | ||||
|       {id: 3, name: 'Saint-Gervais', code: '85230', country: 'FR'} | ||||
|    ]; | ||||
|    const url = `/api/1.0/main/postal-code.json?item_per_page=1000&country=${country.id}`; | ||||
|    return fetch(url) | ||||
|       .then(response => { | ||||
|          if (response.ok) { return response.json(); } | ||||
|          throw Error('Error with request resource response'); | ||||
|       }); | ||||
| }; | ||||
|  | ||||
| /* | ||||
| * Endpoint chill_main_address_reference_api_show | ||||
| * Endpoint chill_api_single_address_reference__index | ||||
| * method GET, get AddressReference Object | ||||
| * @returns {Promise} a promise containing all AddressReference object | ||||
| * @returns {Promise} a promise containing all AddressReference objects filtered with postal code | ||||
| */ | ||||
| const fetchReferenceAddresses = (city) => { | ||||
|     console.log('<<< fetching references addresses for', city); // city n'est pas utilisé pour le moment | ||||
|      | ||||
|     const url = `/api/1.0/main/address-reference.json`; | ||||
| const fetchReferenceAddresses = (postalCode) => { | ||||
|     console.log('<<< fetching references addresses for', postalCode); | ||||
|    //TODO deal with huge number of addresses... we should do suggestion... | ||||
|     const url = `/api/1.0/main/address-reference.json?item_per_page=1000&postal_code=${postalCode.id}`; | ||||
|     return fetch(url) | ||||
|        .then(response => { | ||||
|           if (response.ok) { return response.json(); } | ||||
| @@ -39,8 +45,31 @@ const fetchReferenceAddresses = (city) => { | ||||
|        }); | ||||
| }; | ||||
|  | ||||
| /* | ||||
| * Endpoint chill_api_single_address__entity__create | ||||
| * method POST, post Address Object | ||||
| * @returns {Promise} | ||||
| */ | ||||
| const postAddress = (address) => { | ||||
|    console.log(address); | ||||
|    const url = `/api/1.0/main/address.json?`; | ||||
|    const body = address; | ||||
|  | ||||
|    return fetch(url, { | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|          'Content-Type': 'application/json;charset=utf-8' | ||||
|          }, | ||||
|       body: JSON.stringify(body) | ||||
|    }).then(response => { | ||||
|       if (response.ok) { return response.json(); } | ||||
|       throw Error('Error with request resource response'); | ||||
|    }); | ||||
| }; | ||||
|  | ||||
| export { | ||||
|    fetchCountries, | ||||
|    fetchCities, | ||||
|    fetchReferenceAddresses | ||||
|    fetchReferenceAddresses, | ||||
|    postAddress | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|    <button class="sc-button bt-create centered mt-4" @click="openModal"> | ||||
|       {{ $t('add_an_address') }} | ||||
|       {{ $t('add_an_address_title') }} | ||||
|    </button> | ||||
|  | ||||
|    <teleport to="body"> | ||||
| @@ -9,76 +9,64 @@ | ||||
|          @close="modal.showModal = false"> | ||||
|  | ||||
|          <template v-slot:header> | ||||
|             <h3 class="modal-title">{{ $t('add_an_address') }}</h3> | ||||
|             <h3 class="modal-title">{{ $t('add_an_address_title') }}</h3> | ||||
|          </template> | ||||
|  | ||||
|          <template v-slot:body> | ||||
|              | ||||
|             <h4>{{ $t('select_an_address') }}</h4> | ||||
|              | ||||
|             <label for="isNoAddress"> | ||||
|                <input type="checkbox"  | ||||
|                   name="isNoAddress"  | ||||
|                   v-bind:placeholder="$t('isNoAddress')"  | ||||
|                   v-model="isNoAddress"  | ||||
|                   v-bind:value="value"/> | ||||
|                {{ $t('isNoAddress') }} | ||||
|             </label> | ||||
|             <div class="address_form"> | ||||
|  | ||||
|             <country-selection | ||||
|                v-bind:address="address" | ||||
|                v-bind:getCities="getCities"> | ||||
|             </country-selection> | ||||
|              | ||||
|             <city-selection | ||||
|                v-bind:address="address" | ||||
|                v-bind:getReferenceAddresses="getReferenceAddresses"> | ||||
|             </city-selection> | ||||
|              | ||||
|             <address-selection | ||||
|                v-bind:address="address" | ||||
|                v-bind:updateMapCenter="updateMapCenter"> | ||||
|             </address-selection> | ||||
|                <div class="address_form__header"> | ||||
|                   <h4>{{ $t('select_an_address_title') }}</h4> | ||||
|                </div> | ||||
|  | ||||
|             <address-map | ||||
|                v-bind:address="address" | ||||
|                ref="addressMap"> | ||||
|             </address-map> | ||||
|              | ||||
|             <address-more  | ||||
|                v-if="!isNoAddress" | ||||
|                v-bind:address="address"> | ||||
|             </address-more> | ||||
|                          | ||||
|             <!-- | ||||
|             <div class="address_form__fields__isNoAddress"></div> | ||||
|             <div class="address_form__select"> | ||||
|                <div class="address_form__select__header"></div> | ||||
|                <div class="address_form__select__left"></div> | ||||
|                <div class="address_form__map"></div> | ||||
|                <div class="address_form__select"> | ||||
|  | ||||
|                   <div class="address_form__select__body"> | ||||
|                      <label for="isNoAddress"> | ||||
|                         <input type="checkbox" | ||||
|                            name="isNoAddress" | ||||
|                            v-bind:placeholder="$t('isNoAddress')" | ||||
|                            v-model="isNoAddress" | ||||
|                            v-bind:value="value"/> | ||||
|                         {{ $t('isNoAddress') }} | ||||
|                      </label> | ||||
|  | ||||
|                      <country-selection | ||||
|                         v-bind:address="address" | ||||
|                         v-bind:getCities="getCities"> | ||||
|                      </country-selection> | ||||
|  | ||||
|                      <city-selection | ||||
|                         v-bind:address="address" | ||||
|                         v-bind:getReferenceAddresses="getReferenceAddresses"> | ||||
|                      </city-selection> | ||||
|  | ||||
|                      <address-selection | ||||
|                         v-bind:address="address" | ||||
|                         v-bind:updateMapCenter="updateMapCenter"> | ||||
|                      </address-selection> | ||||
|                   </div> | ||||
|  | ||||
|                   <div class="address_form__select__map"> | ||||
|                      <address-map | ||||
|                         v-bind:address="address" | ||||
|                         ref="addressMap"> | ||||
|                      </address-map> | ||||
|                   </div> | ||||
|                </div> | ||||
|  | ||||
|                <div class="address_form__more"> | ||||
|                   <address-more | ||||
|                      v-if="!isNoAddress" | ||||
|                      v-bind:address="address"> | ||||
|                   </address-more> | ||||
|                </div> | ||||
|             </div> | ||||
|             <div class="address_form__fields"> | ||||
|                <div class="address_form__fields__header"></div> | ||||
|                <div class="address_form__fields__left"></div> | ||||
|                <div class="address_form__fields__right"></div> | ||||
|             </div> | ||||
|           | ||||
|             à discuter,  | ||||
|             mais je pense qu'il est préférable de profiter de l'imbriquation des classes css  | ||||
|              | ||||
|             div.address_form { | ||||
|                div.select { | ||||
|                   div.header {} | ||||
|                   div.left {} | ||||
|                   div.map {} | ||||
|                } | ||||
|             } | ||||
|           | ||||
|             --> | ||||
|  | ||||
|          </template> | ||||
|  | ||||
|          <template v-slot:footer> | ||||
|             <button class="sc-button green"  | ||||
|             <button class="sc-button green" | ||||
|                @click.prevent="$emit('addNewAddress', { address, modal })"> | ||||
|                <i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}} | ||||
|             </button> | ||||
| @@ -155,49 +143,47 @@ export default { | ||||
|          } | ||||
|       } | ||||
|    }, | ||||
|    mounted() { | ||||
|        this.getCountries(); | ||||
|    }, | ||||
|    methods: { | ||||
|       openModal() { | ||||
|          this.modal.showModal = true; | ||||
|          this.resetAll(); | ||||
|          this.getCountries(); | ||||
|           | ||||
|          //this.$nextTick(function() { | ||||
|          //   this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale | ||||
|          //}) | ||||
|       }, | ||||
|       getCountries() { | ||||
|          console.log('getCountries'); | ||||
|          this.address.loaded.countries = fetchCountries(); // à remplacer par | ||||
|          //  fetchCountries().then(countries => new Promise((resolve, reject) => { | ||||
|          //     this.address.loaded.countries = countries; | ||||
|          //     resolve() | ||||
|          //  })) | ||||
|          //  .catch((error) => {  | ||||
|          //     this.errorMsg.push(error.message);  | ||||
|          //  });  | ||||
|          fetchCountries().then(countries => new Promise((resolve, reject) => { | ||||
|              this.address.loaded.countries = countries.results; | ||||
|              resolve() | ||||
|           })) | ||||
|           .catch((error) => { | ||||
|              this.errorMsg.push(error.message); | ||||
|           }); | ||||
|       }, | ||||
|       getCities(country) { | ||||
|          console.log('getCities for', country.name); | ||||
|          this.address.loaded.cities = fetchCities(); // à remplacer par | ||||
|          //  fetchCities(country).then(cities => new Promise((resolve, reject) => { | ||||
|          //     this.address.loaded.cities = cities; | ||||
|          //     resolve() | ||||
|          //  })) | ||||
|          //  .catch((error) => {  | ||||
|          //     this.errorMsg.push(error.message);  | ||||
|          //  });  | ||||
|          fetchCities(country).then(cities => new Promise((resolve, reject) => { | ||||
|             this.address.loaded.cities = cities.results; | ||||
|             resolve() | ||||
|          })) | ||||
|          .catch((error) => { | ||||
|             this.errorMsg.push(error.message); | ||||
|          }); | ||||
|       }, | ||||
|       getReferenceAddresses(city) { | ||||
|          console.log('getReferenceAddresses for', city.name); | ||||
|          fetchReferenceAddresses(city)  // il me semble que le paramètre city va limiter le poids des adresses de références reçues | ||||
|             .then(addresses => new Promise((resolve, reject) => { | ||||
|          fetchReferenceAddresses(city).then(addresses => new Promise((resolve, reject) => { | ||||
|                console.log('addresses', addresses); | ||||
|                this.address.loaded.addresses = addresses.results; | ||||
|                resolve(); | ||||
|             })) | ||||
|             .catch((error) => {  | ||||
|                this.errorMsg.push(error.message);  | ||||
|             });  | ||||
|             .catch((error) => { | ||||
|                this.errorMsg.push(error.message); | ||||
|             }); | ||||
|       }, | ||||
|       updateMapCenter(point) { | ||||
|          console.log('point', point); | ||||
| @@ -208,7 +194,7 @@ export default { | ||||
|       resetAll() { | ||||
|          console.log('reset all selected'); | ||||
|          this.address.loaded.addresses = []; | ||||
|          this.address.selected.address = {};  | ||||
|          this.address.selected.address = {}; | ||||
|          this.address.loaded.cities = []; | ||||
|          this.address.selected.city = {}; | ||||
|          this.address.selected.country = {}; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|    <div class="container"> | ||||
|       <div id='address_map' style='height:400px; width:400px;'></div> | ||||
|       <div id='address_map'></div> | ||||
|    </div> | ||||
| </template> | ||||
|  | ||||
| @@ -11,6 +11,7 @@ import markerIconPng from 'leaflet/dist/images/marker-icon.png' | ||||
| import 'leaflet/dist/leaflet.css'; | ||||
|  | ||||
| let map; | ||||
| let marker; | ||||
|  | ||||
| export default { | ||||
|    name: 'AddressMap', | ||||
| @@ -30,13 +31,15 @@ export default { | ||||
|  | ||||
|          const markerIcon = L.icon({ | ||||
|             iconUrl: markerIconPng, | ||||
|             iconAnchor: [12, 41], | ||||
|          }); | ||||
|  | ||||
|          L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map); | ||||
|          marker = L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map); | ||||
|  | ||||
|       }, | ||||
|       update() { | ||||
|          console.log('update map with : ', this.address.addressMap.center) | ||||
|          marker.setLatLng(this.address.addressMap.center); | ||||
|          map.setView(this.address.addressMap.center, 12); | ||||
|       } | ||||
|    }, | ||||
|   | ||||
| @@ -1,38 +1,56 @@ | ||||
| <template> | ||||
|    <div class="container"> | ||||
|       <select  | ||||
|          v-model="selected"> | ||||
|          <option :value="{}" disabled selected>{{ $t('select_address') }}</option> | ||||
|          <option  | ||||
|             v-for="item in this.addresses"  | ||||
|             v-bind:item="item"  | ||||
|             v-bind:key="item.id"  | ||||
|             v-bind:value="item"> | ||||
|             {{ item.street }}, {{ item.streetNumber }} | ||||
|          </option> | ||||
|       </select> | ||||
|       <VueMultiselect | ||||
|          v-model="value" | ||||
|          @select="selectAddress" | ||||
|          name="field" | ||||
|          track-by="id" | ||||
|          label="value" | ||||
|          :custom-label="transName" | ||||
|          :taggable="true" | ||||
|          :multiple="false" | ||||
|          @tag="addAddress" | ||||
|          :placeholder="$t('select_address')" | ||||
|          :tagPlaceholder="$t('create_address')" | ||||
|          :options="addresses"> | ||||
|       </VueMultiselect> | ||||
|    </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import VueMultiselect from 'vue-multiselect'; | ||||
|  | ||||
| export default { | ||||
|    name: 'AddressSelection', | ||||
|    components: { VueMultiselect }, | ||||
|    props: ['address', 'updateMapCenter'], | ||||
|    data() { | ||||
|       return { | ||||
|          value: null | ||||
|       } | ||||
|    }, | ||||
|    computed: { | ||||
|       addresses() {  | ||||
|          return this.address.loaded.addresses;  | ||||
|       addresses() { | ||||
|          return this.address.loaded.addresses; | ||||
|       } | ||||
|    }, | ||||
|    methods: { | ||||
|       transName(value) { | ||||
|          return value.streetNumber === undefined ? value.street : `${value.street}, ${value.streetNumber}` | ||||
|       }, | ||||
|       selected: { | ||||
|          set(value) { | ||||
|             console.log('selected value', value); | ||||
|             this.address.selected.address = value; | ||||
|             this.updateMapCenter(value.point); | ||||
|          }, | ||||
|          get() { | ||||
|             return this.address.selected.address; | ||||
|          } | ||||
|       selectAddress(value) { | ||||
|          this.address.selected.address = value; | ||||
|          this.updateMapCenter(value.point); | ||||
|       }, | ||||
|       addAddress (newAddress) { | ||||
|          const address = { | ||||
|             street: newAddress | ||||
|          }; | ||||
|          this.value = address; | ||||
|          this.address.selected.address = address; | ||||
|       } | ||||
|    } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style src="vue-multiselect/dist/vue-multiselect.css"></style> | ||||
|   | ||||
| @@ -1,38 +1,45 @@ | ||||
| <template> | ||||
|    <div class="container"> | ||||
|       <select  | ||||
|          v-model="selected"> | ||||
|          <option :value="{}" disabled selected>{{ $t('select_city') }}</option> | ||||
|          <option  | ||||
|             v-for="item in this.cities"  | ||||
|             v-bind:item="item"  | ||||
|             v-bind:key="item.id"  | ||||
|             v-bind:value="item"> | ||||
|             {{ item.code }}-{{ item.name }} | ||||
|          </option> | ||||
|       </select> | ||||
|       <VueMultiselect | ||||
|          v-model="value" | ||||
|          @select="selectCity" | ||||
|          name="field" | ||||
|          track-by="id" | ||||
|          label="value" | ||||
|          :custom-label="transName" | ||||
|          :placeholder="$t('select_city')" | ||||
|          :options="cities"> | ||||
|       </VueMultiselect> | ||||
|    </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import VueMultiselect from 'vue-multiselect'; | ||||
|  | ||||
| export default { | ||||
|    name: 'CitySelection', | ||||
|    components: { VueMultiselect }, | ||||
|    props: ['address', 'getReferenceAddresses'], | ||||
|    data() { | ||||
|       return { | ||||
|          value: null | ||||
|       } | ||||
|    }, | ||||
|    methods: { | ||||
|       transName(value) { | ||||
|          return `${value.code}-${value.name}` | ||||
|       }, | ||||
|       selectCity(value) { | ||||
|          this.address.selected.city = value; | ||||
|          this.getReferenceAddresses(value); | ||||
|       }, | ||||
|    }, | ||||
|    computed: { | ||||
|       cities() {  | ||||
|          return this.address.loaded.cities;  | ||||
|       }, | ||||
|       selected: { | ||||
|          set(value) { | ||||
|             console.log('selected value', value.name); | ||||
|             this.address.selected.city = value; | ||||
|             this.getReferenceAddresses(value); | ||||
|          }, | ||||
|          get() { | ||||
|             return this.address.selected.city; | ||||
|          } | ||||
|       }, | ||||
|       cities() { | ||||
|          return this.address.loaded.cities; | ||||
|       } | ||||
|    } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style src="vue-multiselect/dist/vue-multiselect.css"></style> | ||||
|   | ||||
| @@ -1,38 +1,59 @@ | ||||
| <template> | ||||
|    <div class="container"> | ||||
|       <select  | ||||
|          v-model="selected"> | ||||
|          <option :value="{}" disabled selected>{{ $t('select_country') }}</option> | ||||
|          <option  | ||||
|             v-for="item in this.countries"  | ||||
|             v-bind:item="item"  | ||||
|             v-bind:key="item.id"  | ||||
|             v-bind:value="item"> | ||||
|             {{ item.name }} | ||||
|          </option> | ||||
|       </select> | ||||
|       <VueMultiselect | ||||
|          v-model="value" | ||||
|          name="field" | ||||
|          track-by="id" | ||||
|          label="name" | ||||
|          :custom-label="transName" | ||||
|          :placeholder="$t('select_country')" | ||||
|          :options="countries" | ||||
|          @select="selectCountry"> | ||||
|       </VueMultiselect> | ||||
|    </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import VueMultiselect from 'vue-multiselect'; | ||||
|  | ||||
| export default { | ||||
|    name: 'CountrySelection', | ||||
|    components: { VueMultiselect }, | ||||
|    props: ['address', 'getCities'], | ||||
|    computed: { | ||||
|       countries() {  | ||||
|          return this.address.loaded.countries;  | ||||
|       }, | ||||
|       selected: { | ||||
|          set(value) { | ||||
|             console.log('selected value', value.name); | ||||
|             this.address.selected.country = value; | ||||
|             this.getCities(value); | ||||
|          }, | ||||
|          get() { | ||||
|             return this.address.selected.country; | ||||
|    data() { | ||||
|       return { | ||||
|          value: this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0] | ||||
|       } | ||||
|    }, | ||||
|    methods: { | ||||
|       init() { | ||||
|          if (this.value !== undefined) { | ||||
|             this.getCities(this.value); | ||||
|          } | ||||
|       }, | ||||
|       transName ({ name }) { | ||||
|          return name.fr //TODO multilang | ||||
|       }, | ||||
|       selectCountry(value) { | ||||
|          console.log(value); | ||||
|          this.address.selected.country = value; | ||||
|          this.getCities(value); | ||||
|       }, | ||||
|    }, | ||||
|    mounted(){ | ||||
|       this.init() | ||||
|    }, | ||||
|    computed: { | ||||
|       countries() { | ||||
|          const countries = this.address.loaded.countries; | ||||
|          let orderedCountries = []; | ||||
|          orderedCountries.push(...countries.filter(c => c.countryCode === 'FR')) | ||||
|          orderedCountries.push(...countries.filter(c => c.countryCode === 'BE')) | ||||
|          orderedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE')) | ||||
|          return orderedCountries; | ||||
|       } | ||||
|    } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style src="vue-multiselect/dist/vue-multiselect.css"></style> | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| <div class="timeline"> | ||||
|     {% for result in results %} | ||||
|         <div class="timeline-item {% if loop.index0 is even %}even{% else %}odd{% endif %}"> | ||||
|             {% include result.template with result.template_data %} | ||||
|         </div> | ||||
|     {% endfor %} | ||||
| </div> | ||||
| @@ -1,7 +1,15 @@ | ||||
| <div class="timeline"> | ||||
|     {% for result in results %} | ||||
|         <div class="timeline-item {% if loop.index0 is even %}even{% else %}odd{% endif %}"> | ||||
|             {% include result.template with result.template_data %} | ||||
|         </div> | ||||
|     {% endfor %} | ||||
| </div> | ||||
| {% extends "@ChillMain/layout.html.twig"  %} | ||||
|  | ||||
| {% block content %} | ||||
|   <div id="container content"> | ||||
|     <div class="grid-8 centered"> | ||||
|       <h1>{{ 'Global timeline'|trans }}</h1> | ||||
|  | ||||
|       {{ timeline|raw }} | ||||
|        | ||||
|       {% if nb_items > paginator.getItemsPerPage %} | ||||
|       {{ chill_pagination(paginator) }} | ||||
|       {% endif %} | ||||
|     </div> | ||||
|   </div> | ||||
|   {% endblock content %} | ||||
|   | ||||
| @@ -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'), [ | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,34 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Serializer\Normalizer; | ||||
|  | ||||
| use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||||
| use Symfony\Component\Serializer\Exception\InvalidArgumentException; | ||||
|  | ||||
| use Chill\MainBundle\Doctrine\Model\Point; | ||||
|  | ||||
|  | ||||
| class PointNormalizer implements DenormalizerInterface | ||||
| { | ||||
|  | ||||
|     public function supportsDenormalization($data, string $type, string $format = null) : bool | ||||
|     { | ||||
|         return $type === Point::class; | ||||
|     } | ||||
|  | ||||
|     public function denormalize($data, string $type, string $format = null, array $context = []) | ||||
|     { | ||||
|  | ||||
|         if (!is_array($data)) { | ||||
|             throw new InvalidArgumentException('point data is not an array. It should be an array of 2 coordinates.'); | ||||
|         } else { | ||||
|             if (count($data) !== 2) { | ||||
|                 throw new InvalidArgumentException('point data is not an array of 2 elements. It should be an array of 2 coordinates.'); | ||||
|             } else { | ||||
|                 return Point::fromLonLat($data[0], $data[1]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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 | ||||
|                 )); | ||||
|          | ||||
|   | ||||
							
								
								
									
										155
									
								
								src/Bundle/ChillMainBundle/Timeline/TimelineSingleQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/Bundle/ChillMainBundle/Timeline/TimelineSingleQuery.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Timeline; | ||||
|  | ||||
|  | ||||
| class TimelineSingleQuery | ||||
| { | ||||
|     private ?string $id; | ||||
|  | ||||
|     private ?string $date; | ||||
|  | ||||
|     private ?string $key; | ||||
|  | ||||
|     private ?string $from; | ||||
|  | ||||
|     private ?string $where; | ||||
|  | ||||
|     private array $parameters = []; | ||||
|  | ||||
|     private bool $distinct = false; | ||||
|  | ||||
|     public function __construct( | ||||
|         string $id = null, | ||||
|         string $date = null, | ||||
|         string $key = null, | ||||
|         string $from = null, | ||||
|         string $where = null, | ||||
|         array $parameters = [] | ||||
|     ) { | ||||
|         $this->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; | ||||
|     } | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -31,14 +31,14 @@ services: | ||||
|             - [ setContainer, ["@service_container"]] | ||||
|         tags: | ||||
|             - { name: twig.extension } | ||||
|              | ||||
|  | ||||
|     chill.main.twig.widget: | ||||
|         class: Chill\MainBundle\Templating\Widget\WidgetRenderingTwig | ||||
|         arguments: | ||||
|             - "@event_dispatcher" | ||||
|         tags: | ||||
|             - { name: twig.extension } | ||||
|              | ||||
|  | ||||
|     chill.main.twig.csv_cell: | ||||
|         class: Chill\MainBundle\Templating\CSVCellTwig | ||||
|         tags: | ||||
| @@ -65,3 +65,4 @@ services: | ||||
|             - "@security.authorization_checker" | ||||
|             - "@chill.main.security.authorization.helper" | ||||
|             - "@security.token_storage" | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ services: | ||||
|             $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' | ||||
|         tags: | ||||
|             - { name: console.command } | ||||
|              | ||||
|  | ||||
|     Chill\MainBundle\Command\ChillUserSendRenewPasswordCodeCommand: | ||||
|         arguments: | ||||
|             $logger: '@Psr\Log\LoggerInterface' | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| --- | ||||
| services: | ||||
|      | ||||
|     # note: the autowiring for serializers and normalizers is declared  | ||||
|  | ||||
|     # note: the autowiring for serializers and normalizers is declared | ||||
|     # into ../services.yaml | ||||
|  | ||||
|     Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer: | ||||
|   | ||||
| @@ -4,4 +4,7 @@ services: | ||||
|         arguments: | ||||
|             - "@doctrine.orm.entity_manager" | ||||
|         calls: | ||||
|             - [ setContainer, ["@service_container"]] | ||||
|             - [ setContainer, ["@service_container"]] | ||||
|     # alias: | ||||
|     Chill\MainBundle\Timeline\TimelineBuilder: '@chill_main.timeline_builder' | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -390,13 +390,13 @@ class AccompanyingPeriodController extends AbstractController | ||||
|         /** @var Person $person */ | ||||
|         $person = $this->_getPerson($person_id); | ||||
|  | ||||
|         $criteria = Criteria::create(); | ||||
|         $criteria->where($criteria->expr()->eq('id', $period_id)); | ||||
|  | ||||
|         /* @var $period AccompanyingPeriod */ | ||||
|         $period = $person->getAccompanyingPeriods() | ||||
|                 ->matching($criteria) | ||||
|                 ->first(); | ||||
|         $period = \array_filter( | ||||
|             $person->getAccompanyingPeriods(),  | ||||
|             function (AccompanyingPeriod $p) use ($period_id) { | ||||
|                 return $p->getId() === ($period_id); | ||||
|             } | ||||
|         )[0] ?? NULL; | ||||
|  | ||||
|         if ($period === NULL) { | ||||
|             throw $this->createNotFoundException('period not found'); | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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; | ||||
|     } | ||||
|      | ||||
|      | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| @@ -447,12 +443,13 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface | ||||
|      */ | ||||
|     public function getParticipationsContainsPerson(Person $person): Collection | ||||
|     { | ||||
|         return $this->getParticipations($person)->filter( | ||||
|             function(AccompanyingPeriodParticipation $participation) use ($person) { | ||||
|                 if ($person === $participation->getPerson()) { | ||||
|                     return $participation; | ||||
|         return $this | ||||
|             ->getParticipations() | ||||
|             ->filter( | ||||
|                 static function(AccompanyingPeriodParticipation $participation) use ($person): bool { | ||||
|                     return $person === $participation->getPerson(); | ||||
|                 } | ||||
|             }); | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -462,12 +459,13 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface | ||||
|      */ | ||||
|     public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation | ||||
|     { | ||||
|         $collection = $this->getParticipationsContainsPerson($person)->filter( | ||||
|             function(AccompanyingPeriodParticipation $participation) use ($person) { | ||||
|                 if (NULL === $participation->getEndDate()) { | ||||
|                     return $participation; | ||||
|         $collection = $this | ||||
|             ->getParticipationsContainsPerson($person) | ||||
|             ->filter( | ||||
|                 static function(AccompanyingPeriodParticipation $participation): bool { | ||||
|                     return null === $participation->getEndDate(); | ||||
|                 } | ||||
|             }); | ||||
|             ); | ||||
|  | ||||
|         return $collection->count() > 0 ? $collection->first() : NULL; | ||||
|     } | ||||
| @@ -557,15 +555,16 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         $participation = $this->getParticipationsContainsPerson($person); | ||||
|         if (!null === $participation) | ||||
|         $participation = $this->getOpenParticipationContainsPerson($person); | ||||
|  | ||||
|         if (null === $participation) | ||||
|         { | ||||
|             $person = $participation->getPerson(); | ||||
|             $periods = $person->getAccompanyingPeriodsOrdered(); | ||||
|             return end($periods) === $this; | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|         $periods = $participation->getPerson()->getAccompanyingPeriodsOrdered(); | ||||
|  | ||||
|         return end($periods) === $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -825,14 +824,18 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface | ||||
|  | ||||
|     /** | ||||
|      * Get a list of all persons which are participating to this course | ||||
|      * | ||||
|      * @psalm-return Collection<int, Person> | ||||
|      */ | ||||
|     public function getPersons(): Collection | ||||
|     { | ||||
|         return $this->participations->map( | ||||
|             function(AccompanyingPeriodParticipation $participation) { | ||||
|                 return $participation->getPerson(); | ||||
|             } | ||||
|         ); | ||||
|         return $this | ||||
|             ->participations | ||||
|             ->map( | ||||
|                 static function(AccompanyingPeriodParticipation $participation): Person { | ||||
|                     return $participation->getPerson(); | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     public function setCreatedAt(\DateTimeInterface $datetime): self | ||||
|   | ||||
| @@ -21,31 +21,19 @@ namespace Chill\PersonBundle\Form\ChoiceLoader; | ||||
|  | ||||
| use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; | ||||
| use Symfony\Component\Form\ChoiceList\ChoiceListInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
|  | ||||
| /** | ||||
|  * Class PersonChoiceLoader | ||||
|  * | ||||
|  * @package Chill\PersonBundle\Form\ChoiceLoader | ||||
|  * @author Julien Fastré <julien.fastre@champs-libres.coop> | ||||
|  * Allow to load a list of person | ||||
|  */ | ||||
| class PersonChoiceLoader implements ChoiceLoaderInterface | ||||
| { | ||||
|     /** | ||||
|      * @var EntityRepository | ||||
|      */ | ||||
|     protected $personRepository; | ||||
|     protected PersonRepository $personRepository; | ||||
|      | ||||
|     /** | ||||
|      * @var array | ||||
|      */ | ||||
|     protected $lazyLoadedPersons = []; | ||||
|     protected array $lazyLoadedPersons = []; | ||||
|      | ||||
|     /** | ||||
|      * @var array | ||||
|      */ | ||||
|     protected $centers = []; | ||||
|     protected array $centers = []; | ||||
|      | ||||
|     /** | ||||
|      * PersonChoiceLoader constructor. | ||||
| @@ -54,7 +42,7 @@ class PersonChoiceLoader implements ChoiceLoaderInterface | ||||
|      * @param array|null $centers | ||||
|      */ | ||||
|     public function __construct( | ||||
|         EntityRepository $personRepository, | ||||
|         PersonRepository $personRepository, | ||||
|         array $centers = null | ||||
|     ) { | ||||
|         $this->personRepository = $personRepository; | ||||
|   | ||||
| @@ -1,11 +1,24 @@ | ||||
| <h3 class="single-line"> | ||||
| <div> | ||||
|   <h3 class="single-line"> | ||||
|     {{ period.closingDate|format_date('long')  }} | ||||
|     <span class="person"> / | ||||
|         <a href="{{ path('chill_person_accompanying_period_list', { 'person_id': person.id } ) }}"> | ||||
|             {{ 'Closing the accompanying period' | trans }} | ||||
|         </a> | ||||
|     <span> | ||||
|     <span class="chill-red"> | ||||
|         <i class="fa fa-folder"></i> | ||||
|     </span> | ||||
| </h3>  | ||||
|     <span class="person"> / </span> | ||||
|     {{ 'An accompanying period ends'|trans }} | ||||
|     {% if 'person' != context %} | ||||
|       {% for p in period.persons %} | ||||
|         / {{ p|chill_entity_render_box({'addLink': true}) }} | ||||
|       {% endfor %} | ||||
|     {% endif %} | ||||
|   </h3> | ||||
|  | ||||
|   <div class="statement"> | ||||
|     <dl class="chill_view_data"> | ||||
|       <dd>{{ 'Participants'|trans }} :</dd> | ||||
|       <dt> | ||||
|       <ul> | ||||
|         {% for p in period.participations %} | ||||
|         <li>{{ 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") }) }}</li> | ||||
|         {% endfor %} | ||||
|       </ul> | ||||
|       </dt> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,11 +1,24 @@ | ||||
| <h3 class="single-line"> | ||||
| <div> | ||||
|   <h3 class="single-line"> | ||||
|     {{ period.openingDate|format_date('long')  }} | ||||
|     <span class="person"> / | ||||
|         <a href="{{ path('chill_person_accompanying_period_list', { 'person_id': person.id } ) }}"> | ||||
|             {{ 'Opening the accompanying period' | trans }} | ||||
|         </a> | ||||
|     </span> | ||||
|     <span class="chill-green"> | ||||
|         <i class="fa fa-folder-open"></i> | ||||
|     </span> | ||||
| </h3>  | ||||
|     <span class="person"> / </span> | ||||
|     {{ 'An accompanying period starts'|trans }} | ||||
|     {% if 'person' != context %} | ||||
|       {% for p in period.persons %} | ||||
|         / {{ p|chill_entity_render_box({'addLink': true}) }} | ||||
|       {% endfor %} | ||||
|     {% endif %} | ||||
|   </h3> | ||||
|  | ||||
|   <div class="statement"> | ||||
|     <dl class="chill_view_data"> | ||||
|       <dd>{{ 'Participants'|trans }} :</dd> | ||||
|       <dt> | ||||
|       <ul> | ||||
|         {% for p in period.participations %} | ||||
|         <li>{{ 'Since %date%'|trans( {'%date%': p.startDate|format_date("long") } ) }} : {{ p.person|chill_entity_render_box({ 'addLink': true }) }}</li> | ||||
|         {% endfor %} | ||||
|       </ul> | ||||
|       </dt> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -14,14 +14,10 @@ class SocialIssueNormalizer implements NormalizerInterface, NormalizerAwareInter | ||||
|  | ||||
|     use NormalizerAwareTrait; | ||||
|  | ||||
|     /** | ||||
|      * @param SocialIssueRender $render | ||||
|      */ | ||||
|     public function __construct(SocialIssueRender $render) | ||||
|     { | ||||
|         $this->render = $render; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     public function normalize($socialIssue, string $format = null, array $context = []) | ||||
|     { | ||||
|   | ||||
| @@ -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'). | ||||
|             '<span class="chill-entity__person__first-name">'.$person->getFirstName().'</span>'. | ||||
|             ' <span class="chill-entity__person__last-name">'.$person->getLastName().'</span>'. | ||||
|             $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 = ''; | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class AccompanyingCourseControllerTest extends WebTestCase | ||||
|         $this->assertResponseRedirects(); | ||||
|         $location = $this->client->getResponse()->headers->get('Location'); | ||||
|  | ||||
|         $this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location)); | ||||
|         $this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/edit$|", $location)); | ||||
|           | ||||
|     } | ||||
|  | ||||
| @@ -48,7 +48,7 @@ class AccompanyingCourseControllerTest extends WebTestCase | ||||
|         $location = $this->client->getResponse()->headers->get('Location'); | ||||
|         $matches = []; | ||||
|  | ||||
|         $this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location, $matches)); | ||||
|         $this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/edit$|", $location, $matches)); | ||||
|         $id = $matches[1]; | ||||
|  | ||||
|         $period = self::$container->get(EntityManagerInterface::class) | ||||
|   | ||||
| @@ -148,7 +148,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      * Test the closing of a periods | ||||
|      *  | ||||
|      * Given that a person as an accompanying period opened since 2015-01-05 | ||||
|      * and we fill the close form (at /en/person/[id]/accompanying-period/close | ||||
|      * and we fill the close form (at /fr/person/[id]/accompanying-period/close | ||||
|      *      with : dateClosing: 2015-02-01 | ||||
|      *      with : the last closing motive in list | ||||
|      * Then the response should redirect to period view | ||||
| @@ -158,10 +158,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testClosingCurrentPeriod() | ||||
|     {    | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/close'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Close accompanying period')->form(); | ||||
|  | ||||
|         $form = $crawler->selectButton('Clôre la période')->form(); | ||||
|  | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
| @@ -171,7 +171,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|         $cr = $this->client->submit($form); | ||||
|          | ||||
|         $this->assertTrue($this->client->getResponse()->isRedirect( | ||||
|               '/en/person/'.$this->person->getId().'/accompanying-period'), | ||||
|               '/fr/person/'.$this->person->getId().'/accompanying-period'), | ||||
|               'the server redirects to /accompanying-period page'); | ||||
|         $this->assertGreaterThan(0, $this->client->followRedirect() | ||||
|                 ->filter('.success')->count(), | ||||
| @@ -182,7 +182,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      * Test the closing of a periods | ||||
|      *  | ||||
|      * Given that a person as an accompanying period opened since 2015-01-05 | ||||
|      * and we fill the close form (at /en/person/[id]/accompanying-period/close | ||||
|      * and we fill the close form (at /fr/person/[id]/accompanying-period/close | ||||
|      *      with : dateClosing: 2014-01-01 | ||||
|      *      with : the last closing motive in list | ||||
|      * Then the response should redirect to period view | ||||
| @@ -192,10 +192,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testClosingCurrentPeriodWithDateClosingBeforeOpeningFails() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/close'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Close accompanying period')->form(); | ||||
|         $form = $crawler->selectButton('Clôre la période')->form(); | ||||
|  | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
| @@ -223,10 +223,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testAddNewPeriodBeforeActual() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton('Créer une période d\'accompagnement')->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -237,7 +237,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|         $this->client->submit($form); | ||||
|          | ||||
|         $this->assertTrue($this->client->getResponse()->isRedirect( | ||||
|               '/en/person/'.$this->person->getId().'/accompanying-period'), | ||||
|               '/fr/person/'.$this->person->getId().'/accompanying-period'), | ||||
|               'the server redirects to /accompanying-period page'); | ||||
|         $this->assertGreaterThan(0, $this->client->followRedirect() | ||||
|               ->filter('.success')->count(), | ||||
| @@ -257,10 +257,13 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testCreatePeriodWithClosingAfterCurrentFails() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $this->markTestSkipped("Multiple period may now cover. This test is kept ". | ||||
|             "in case of a configuration may add this feature again"); | ||||
|  | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton("Créer une période d'accompagnement")->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -289,10 +292,13 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testCreatePeriodWithOpeningAndClosingAfterCurrentFails() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $this->markTestSkipped("Multiple period may now cover. This test is kept ". | ||||
|             "in case of a configuration may add this feature again"); | ||||
|  | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton("Créer une période d'accompagnement")->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -330,10 +336,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|             ] | ||||
|         )); | ||||
|          | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form();; | ||||
|         $form = $crawler->selectButton('Créer une période d\'accompagnement')->form();; | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -361,10 +367,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|      */ | ||||
|     public function testCreatePeriodWithClosingBeforeOpeningFails() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton('Créer une période d\'accompagnement')->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -403,10 +409,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|             ] | ||||
|         )); | ||||
|          | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton('Créer une période d\'accompagnement')->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -444,10 +450,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|             ] | ||||
|         )); | ||||
|          | ||||
|         $crawler = $this->client->request('GET', '/en/person/' | ||||
|         $crawler = $this->client->request('GET', '/fr/person/' | ||||
|               .$this->person->getId().'/accompanying-period/create'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Create an accompanying period')->form(); | ||||
|         $form = $crawler->selectButton('Créer une période d\'accompagnement')->form(); | ||||
|         $form->get(self::CLOSING_MOTIVE_INPUT) | ||||
|               ->setValue($this->getLastValueOnClosingMotive($form)); | ||||
|         $form->get(self::CLOSING_INPUT) | ||||
| @@ -498,7 +504,8 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|         //$criteria->where(Criteria::expr()->eq('openingDate', \DateTime::createFromFormat())) | ||||
|         $firstPeriod = reset($periods); | ||||
|         $lastPeriod = end($periods); | ||||
|          | ||||
|  | ||||
|         $this->markTestSkipped("From here, the test should be rewritten");  | ||||
|         // test that it is not possible to open the first period in the list | ||||
|         $this->client->request('GET',  | ||||
|                 sprintf('/fr/person/%d/accompanying-period/%d/re-open', $this->person->getId(), reset($periods)->getId()) | ||||
| @@ -523,4 +530,4 @@ class AccompanyingPeriodControllerTest extends WebTestCase | ||||
|                 "Test the response is a redirection => the period is re-opened"); | ||||
|  | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\PersonBundle\Tests\Controller; | ||||
|  | ||||
| use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | ||||
|  | ||||
| class AdminControllerTest extends WebTestCase | ||||
| { | ||||
|     public function testIndex() | ||||
|     { | ||||
|         $client = static::createClient(); | ||||
|  | ||||
|         $crawler = $client->request('GET', '/{_locale}/admin/person'); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -25,7 +25,7 @@ namespace Chill\PersonBundle\Tests\Controller; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | ||||
| use Symfony\Component\DomCrawler\Form; | ||||
| use Chill\MainBundle\Test\PrepareClientTrait; | ||||
| use \Symfony\Component\BrowserKit\Client; | ||||
| use Symfony\Bundle\FrameworkBundle\KernelBrowser; | ||||
|  | ||||
| /** | ||||
|  * Test creation and deletion for persons | ||||
| @@ -34,7 +34,7 @@ class PersonControllerCreateTest extends WebTestCase | ||||
| { | ||||
|     use PrepareClientTrait; | ||||
|  | ||||
|     private Client $client; | ||||
|     private KernelBrowser $client; | ||||
|  | ||||
|     const FIRSTNAME_INPUT = 'chill_personbundle_person_creation[firstName]'; | ||||
|     const LASTNAME_INPUT = "chill_personbundle_person_creation[lastName]"; | ||||
| @@ -59,8 +59,8 @@ class PersonControllerCreateTest extends WebTestCase | ||||
|         string $firstname = 'God', | ||||
|         string $lastname = 'Jesus' | ||||
|     ) { | ||||
|         $creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname); | ||||
|         $creationForm->get(self::LASTNAME_INPUT)->setValue($lastname); | ||||
|         $creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname.'_'.uniqid()); | ||||
|         $creationForm->get(self::LASTNAME_INPUT)->setValue($lastname.'_'.uniqid()); | ||||
|         $creationForm->get(self::GENDER_INPUT)->select("man"); | ||||
|         $date = new \DateTime('1947-02-01'); | ||||
|         $creationForm->get(self::BIRTHDATE_INPUT)->setValue($date->format('d-m-Y')); | ||||
| @@ -114,20 +114,6 @@ class PersonControllerCreateTest extends WebTestCase | ||||
|         return $form; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      *  | ||||
|      * @param Form $form | ||||
|      * @depends testAddAPersonPage | ||||
|      */ | ||||
|     public function testForgedNullGender(Form $form) | ||||
|     { | ||||
|         $form->get(self::FIRSTNAME_INPUT)->setValue('john'); | ||||
|         $form->get(self::LASTNAME_INPUT)->setValue('doe'); | ||||
|         $date = new \DateTime('1947-02-01'); | ||||
|         $form->get(self::BIRTHDATE_INPUT)->setValue($date->format('d-m-Y')); | ||||
|         $this->client->submit($form); | ||||
|         $this->assertResponseStatusCodeSame(500); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Test the creation of a valid person. | ||||
| @@ -140,8 +126,8 @@ class PersonControllerCreateTest extends WebTestCase | ||||
|     { | ||||
|         $this->fillAValidCreationForm($form); | ||||
|         $client = $this->client; | ||||
|         $client->submit($form); | ||||
|          | ||||
|         $crawler = $client->submit($form); | ||||
|  | ||||
|         $this->assertTrue((bool)$client->getResponse()->isRedirect(), | ||||
|             "a valid form redirect to url /{_locale}/person/{personId}/general/edit"); | ||||
|         $client->followRedirect(); | ||||
|   | ||||
| @@ -20,18 +20,16 @@ | ||||
|  | ||||
| namespace Chill\PersonBundle\Tests\Controller; | ||||
|  | ||||
| //ini_set('memory_limit', '-1'); | ||||
|  | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | ||||
| use Chill\MainBundle\Test\PrepareClientTrait; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Test the edition of persons | ||||
|  *  | ||||
|  * As I am logged in as "center a_social" | ||||
|  * | ||||
|  * @author Julien Fastré <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| class PersonControllerUpdateTest extends WebTestCase | ||||
| { | ||||
| @@ -71,8 +69,8 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|         $this->em->persist($this->person); | ||||
|         $this->em->flush(); | ||||
|          | ||||
|         $this->editUrl = '/en/person/'.$this->person->getId().'/general/edit'; | ||||
|         $this->viewUrl  = '/en/person/'.$this->person->getId().'/general'; | ||||
|         $this->editUrl = '/fr/person/'.$this->person->getId().'/general/edit'; | ||||
|         $this->viewUrl  = '/fr/person/'.$this->person->getId().'/general'; | ||||
|          | ||||
|         $this->client = $this->getClientAuthenticated(); | ||||
|     } | ||||
| @@ -104,10 +102,10 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|     public function testHiddenFielsArePresent() | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', $this->editUrl); | ||||
|          | ||||
|  | ||||
|         $configurables = array('placeOfBirth', 'phonenumber', 'email', | ||||
|             'countryOfBirth', 'nationality', 'spokenLanguages', 'maritalStatus'); | ||||
|         $form = $crawler->selectButton('Submit')->form(); //; | ||||
|         $form = $crawler->selectButton('Enregistrer')->form(); //; | ||||
|          | ||||
|         foreach($configurables as $key) { | ||||
|             $this->assertTrue($form->has('chill_personbundle_person['.$key.']')); | ||||
| @@ -162,18 +160,18 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', $this->editUrl); | ||||
|          | ||||
|         $form = $crawler->selectButton('Submit') | ||||
|         $form = $crawler->selectButton('Enregistrer') | ||||
|             ->form(); | ||||
|         //transform countries into value if needed | ||||
|         switch ($field) { | ||||
|             case 'nationality': | ||||
|             case 'countryOfBirth': | ||||
|                 if ($value !== NULL) { | ||||
|                 if (FALSE === empty($value)) { | ||||
|                     $country = $this->em->getRepository('ChillMainBundle:Country') | ||||
|                         ->findOneByCountryCode($value); | ||||
|                     $transformedValue = $country->getId(); | ||||
|                 } else { | ||||
|                     $transformedValue = NULL; | ||||
|                     $transformedValue = ''; | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
| @@ -208,7 +206,7 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|         $crawler = $this->client->request('GET', $this->editUrl); | ||||
|         $selectedLanguages = array('en', 'an', 'bbj'); | ||||
|          | ||||
|         $form = $crawler->selectButton('Submit') | ||||
|         $form = $crawler->selectButton('Enregistrer') | ||||
|             ->form(); | ||||
|         $form->get('chill_personbundle_person[spokenLanguages]') | ||||
|             ->setValue($selectedLanguages); | ||||
| @@ -238,7 +236,7 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|     { | ||||
|         $crawler = $this->client->request('GET', $this->editUrl); | ||||
|          | ||||
|         $form = $crawler->selectButton('Submit') | ||||
|         $form = $crawler->selectButton('Enregistrer') | ||||
|             ->form(); | ||||
|         $form->get('chill_personbundle_person['.$field.']') | ||||
|             ->setValue($value); | ||||
| @@ -264,7 +262,7 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|             ['lastName' , 'random Value', function(Person $person) { return $person->getLastName(); }  ], | ||||
|             ['placeOfBirth', 'none place', function(Person $person) { return $person->getPlaceOfBirth(); }], | ||||
|             ['birthdate', '15-12-1980', function(Person $person) { return $person->getBirthdate()->format('d-m-Y'); }], | ||||
|             ['phonenumber', '0123456789', function(Person $person) { return $person->getPhonenumber(); }], | ||||
|             ['phonenumber', '+32123456789', function(Person $person) { return $person->getPhonenumber(); }], | ||||
|             ['memo', 'jfkdlmq jkfldmsq jkmfdsq', function(Person $person) { return $person->getMemo(); }], | ||||
|             ['countryOfBirth', 'BE', function(Person $person) { return $person->getCountryOfBirth()->getCountryCode(); }], | ||||
|             ['nationality', 'FR', function(Person $person) { return $person->getNationality()->getCountryCode(); }], | ||||
| @@ -275,7 +273,6 @@ class PersonControllerUpdateTest extends WebTestCase | ||||
|             ['countryOfBirth', NULL, function(Person $person) { return $person->getCountryOfBirth(); }], | ||||
|             ['nationality', NULL, function(Person $person) { return $person->getNationality(); }], | ||||
|             ['gender', Person::FEMALE_GENDER, function(Person $person) { return $person->getGender(); }], | ||||
|             ['maritalStatus', NULL, function(Person $person) {return $person->getMaritalStatus(); }] | ||||
|         ); | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -22,10 +22,6 @@ namespace Chill\PersonBundle\Tests\Controller; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
|  | ||||
| /** | ||||
|  * @author Julien Fastré <julien.fastre@champs-libres.coop> | ||||
|  * @author Marc Ducobu <marc.ducobu@champs-libres.coop> | ||||
|  */ | ||||
| class PersonControllerViewTestWithHiddenFields extends WebTestCase | ||||
| { | ||||
|     /** @var \Doctrine\ORM\EntityManagerInterface The entity manager */ | ||||
| @@ -66,6 +62,7 @@ class PersonControllerViewTestWithHiddenFields extends WebTestCase | ||||
|      */ | ||||
|     public function testViewPerson() | ||||
|     { | ||||
|         $this->markTestSkipped("This configuration does not allow multiple environnements"); | ||||
|         $client = static::createClient( | ||||
|                 array('environment' => 'test_with_hidden_fields'),  | ||||
|                 array( | ||||
|   | ||||
| @@ -26,7 +26,6 @@ use Chill\PersonBundle\Form\Type\PickPersonType; | ||||
| /** | ||||
|  *  | ||||
|  * | ||||
|  * @author Julien Fastré <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| class PickPersonTypeTest extends KernelTestCase | ||||
| { | ||||
| @@ -59,6 +58,8 @@ class PickPersonTypeTest extends KernelTestCase | ||||
|      | ||||
|     public function testWithoutOption() | ||||
|     { | ||||
|         $this->markTestSkipped("need to inject locale into url generator without request"); | ||||
|  | ||||
|         $form = $this->formFactory | ||||
|                 ->createBuilder(PickPersonType::class, null, array()) | ||||
|                 ->getForm(); | ||||
| @@ -86,7 +87,8 @@ class PickPersonTypeTest extends KernelTestCase | ||||
|      */ | ||||
|     public function testWithOptionCenter() | ||||
|     { | ||||
|         $center = $this->container->get('doctrine.orm.entity_manager') | ||||
|         $this->markTestSkipped("need to inject locale into url generator without request"); | ||||
|         $center = self::$container->get('doctrine.orm.entity_manager') | ||||
|                 ->getRepository('ChillMainBundle:Center') | ||||
|                 ->findOneBy(array('name' => 'Center A')) | ||||
|                 ; | ||||
| @@ -117,7 +119,8 @@ class PickPersonTypeTest extends KernelTestCase | ||||
|      */ | ||||
|     public function testWithOptionCenters() | ||||
|     { | ||||
|         $centers = $this->container->get('doctrine.orm.entity_manager') | ||||
|         $this->markTestSkipped("need to inject locale into url generator without request"); | ||||
|         $centers = self::$container->get('doctrine.orm.entity_manager') | ||||
|                 ->getRepository('ChillMainBundle:Center') | ||||
|                 ->findAll() | ||||
|                 ; | ||||
| @@ -149,6 +152,7 @@ class PickPersonTypeTest extends KernelTestCase | ||||
|     public function testWithInvalidOptionCenters() | ||||
|     { | ||||
|          | ||||
|         $this->markTestSkipped("need to inject locale into url generator without request"); | ||||
|         $form = $this->formFactory | ||||
|                 ->createBuilder(PickPersonType::class, null, array( | ||||
|                     'centers' => array('string') | ||||
| @@ -158,6 +162,7 @@ class PickPersonTypeTest extends KernelTestCase | ||||
|      | ||||
|     public function testWithOptionRoleInvalid() | ||||
|     { | ||||
|         $this->markTestSkipped("need to inject locale into url generator without request"); | ||||
|         $form = $this->formFactory | ||||
|                 ->createBuilder(PickPersonType::class, null, array( | ||||
|                     'role' => new \Symfony\Component\Security\Core\Role\Role('INVALID') | ||||
|   | ||||
| @@ -52,75 +52,75 @@ class PersonSearchTest extends WebTestCase | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByFirstName() | ||||
|     public function testSearchByLastName() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByFirstNameLower() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:depardieu'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Gérard'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByFirstNamePartim() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Dep'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Ger'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testFirstNameAccentued() | ||||
|     public function testLastNameAccentued() | ||||
|     { | ||||
|         $crawlerSpecial = $this->generateCrawlerForSearch('@person firstname:manço'); | ||||
|         $crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:manço'); | ||||
|          | ||||
|         $this->assertRegExp('/Manço/', $crawlerSpecial->text()); | ||||
|          | ||||
|          | ||||
|         $crawlerNoSpecial = $this->generateCrawlerForSearch('@person firstname:manco'); | ||||
|         $crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:manco'); | ||||
|          | ||||
|         $this->assertRegExp('/Manço/', $crawlerNoSpecial->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByLastName() | ||||
|     | ||||
|     public function testSearchByFirstName() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:Jean'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Jean'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByLastNameLower() | ||||
|     public function testSearchByFirstNameLower2() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:jean'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:jean'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByLastNamePartim() | ||||
|     public function testSearchByFirstNamePartim2() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:ean'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:ean'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchByLastNameAccented() | ||||
|     public function testSearchByFirstNameAccented() | ||||
|     { | ||||
|         $crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:Gérard'); | ||||
|         $crawlerSpecial = $this->generateCrawlerForSearch('@person firstname:Gérard'); | ||||
|          | ||||
|         $this->assertRegExp('/Gérard/', $crawlerSpecial->text()); | ||||
|          | ||||
|          | ||||
|         $crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:Gerard'); | ||||
|         $crawlerNoSpecial = $this->generateCrawlerForSearch('@person firstname:Gerard'); | ||||
|          | ||||
|         $this->assertRegExp('/Gérard/', $crawlerNoSpecial->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchCombineFirstnameAndNationality() | ||||
|     public function testSearchCombineLastnameAndNationality() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu nationality:RU'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu nationality:RU'); | ||||
|          | ||||
|         $this->assertRegExp('/Gérard/', $crawler->text()); | ||||
|         //if this is a AND clause, Jean Depardieu should not appears | ||||
| @@ -130,7 +130,7 @@ class PersonSearchTest extends WebTestCase | ||||
|      | ||||
|     public function testSearchCombineLastnameAndFirstName() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu lastname:Jean'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu firstname:Jean'); | ||||
|          | ||||
|         $this->assertRegExp('/Depardieu/', $crawler->text()); | ||||
|         //if this is a AND clause, Jean Depardieu should not appears | ||||
| @@ -146,17 +146,17 @@ class PersonSearchTest extends WebTestCase | ||||
|         $this->assertRegExp('/Bart/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchCombineBirthdateAndFirstName() | ||||
|     public function testSearchCombineBirthdateAndLastName() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 firstname:(Van Snick)'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 lastname:(Van Snick)'); | ||||
|          | ||||
|         $this->assertRegExp('/Bart/', $crawler->text()); | ||||
|         $this->assertNotRegExp('/Depardieu/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|     public function testSearchCombineGenderAndFirstName() | ||||
|     public function testSearchCombineGenderAndLastName() | ||||
|     { | ||||
|         $crawler = $this->generateCrawlerForSearch('@person gender:woman firstname:(Depardieu)'); | ||||
|         $crawler = $this->generateCrawlerForSearch('@person gender:woman lastname:(Depardieu)'); | ||||
|          | ||||
|         $this->assertRegExp('/Charline/', $crawler->text()); | ||||
|         $this->assertNotRegExp('/Gérard/', $crawler->text()); | ||||
| @@ -171,8 +171,6 @@ class PersonSearchTest extends WebTestCase | ||||
|         $this->assertNotRegExp('/Jean/', $crawler->text()); | ||||
|     } | ||||
|      | ||||
|      | ||||
|      | ||||
|     public function testDefaultAccented() | ||||
|     { | ||||
|         $crawlerSpecial = $this->generateCrawlerForSearch('@person manço'); | ||||
| @@ -215,7 +213,7 @@ class PersonSearchTest extends WebTestCase | ||||
|         $client = $this->getAuthenticatedClient($username); | ||||
|          | ||||
|         $crawler = $client->request('GET', '/fr/search', array( | ||||
|            'q' => $pattern | ||||
|            'q' => $pattern, | ||||
|         )); | ||||
|          | ||||
|         $this->assertTrue($client->getResponse()->isSuccessful()); | ||||
|   | ||||
| @@ -50,10 +50,10 @@ class TimelineAccompanyingPeriodTest extends \Chill\PersonBundle\Tests\Controlle | ||||
|               "the timeline page loads sucessfully"); | ||||
|         $this->assertGreaterThan(0, $crawler->filter('.timeline div')->count(), | ||||
|               "the timeline page contains multiple div inside a .timeline element"); | ||||
|         $this->assertContains("Ouverture d'une période d'accompagnement", | ||||
|         $this->assertContains(" Une période d'accompagnement est ouverte", | ||||
|               $crawler->filter('.timeline')->text(), | ||||
|               "the text 'une période d'accompagnement a été ouverte' is present"); | ||||
|         $this->assertContains("Fermeture de la période d'accompagnement", | ||||
|         $this->assertContains("Une periode d'accompagnement se clôture", | ||||
|               $crawler->Filter('.timeline')->text(), | ||||
|               "the text 'Une période d'accompagnement a été fermée' is present"); | ||||
|     } | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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] | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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 ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -24,18 +24,32 @@ 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.security.authorization.person: | ||||
|         class: Chill\PersonBundle\Security\Authorization\PersonVoter | ||||
|         arguments: | ||||
|             - "@chill.main.security.authorization.helper" | ||||
|         tags: | ||||
|             - { name: security.voter } | ||||
|             - { name: chill.role } | ||||
|              | ||||
|     chill.person.birthdate_validation: | ||||
|         class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator | ||||
|         arguments: | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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' | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| @@ -165,6 +167,7 @@ Pediod closing form is not valid: Le formulaire n'est pas valide | ||||
| Accompanying user: Accompagnant | ||||
| No accompanying user: Aucun accompagnant | ||||
| No data given: Pas d'information | ||||
| Participants: Personnes impliquées | ||||
| Create an accompanying course: Créer un parcours | ||||
| This accompanying course is still a draft: Ce parcours est à l'état brouillon | ||||
| Associated peoples: Usagers concernés | ||||
| @@ -179,8 +182,6 @@ Referrer: Référent | ||||
| # 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% | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| <div class="report_entry"> | ||||
|     <h3>{{ report.date|format_date('long')  }}<span class="report"> / {{ 'Report'|trans }}</span></h3> | ||||
|   <h3>{{ report.date|format_date('long')  }}<span class="report"> / {{ 'Report'|trans }}</span>{% if 'person' != context %} / {{ report.person|chill_entity_render_box }}{% endif %}</h3> | ||||
|     <div class="statement"> | ||||
|         <span class="statement">{{ '%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') } | ||||
|         ) }}</span> | ||||
| @@ -25,13 +25,13 @@ | ||||
|       | ||||
|      <ul class="record_actions"> | ||||
|         <li> | ||||
|             <a href="{{ path('report_view', { 'person_id': person.id, 'report_id': report.id} ) }}" class="sc-button bt-view"> | ||||
|             <a href="{{ path('report_view', { 'person_id': report.person.id, 'report_id': report.id} ) }}" class="sc-button bt-view"> | ||||
|                 {{ 'View the report'|trans }} | ||||
|             </a> | ||||
|         </li> | ||||
|         {% if is_granted('CHILL_REPORT_UPDATE', report) %} | ||||
|         <li> | ||||
|             <a href="{{ path('report_edit', { 'person_id': person.id, 'report_id': report.id} ) }}" class="sc-button bt-edit"> | ||||
|             <a href="{{ path('report_edit', { 'person_id': report.person.id, 'report_id': report.id} ) }}" class="sc-button bt-edit"> | ||||
|                 {{ 'Update the report'|trans }} | ||||
|             </a> | ||||
|         </li> | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  * @author Champs Libres <info@champs-libres.coop> | ||||
|  */ | ||||
| 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"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 } | ||||
|              | ||||
|              | ||||
|   | ||||
| @@ -7,6 +7,9 @@ | ||||
|         {% else %} | ||||
|             <span class="statement">{{ '%user% has created the task'|trans({ '%user%': event.author.username }) }}</span>  | ||||
|         {% endif %} | ||||
|         {% if 'person' != context %} | ||||
|           / {{ task.person|chill_entity_render_box({'addLink': true}) }} | ||||
|         {% endif %} | ||||
|     </h3> | ||||
|      | ||||
|     <div class="statement"> | ||||
| @@ -29,5 +32,17 @@ | ||||
|                 {% endif %} | ||||
|             </dl> | ||||
|     </div> | ||||
|     <ul class="record_actions"> | ||||
|       <li> | ||||
|         <a href="{{ chill_path_add_return_path('chill_task_single_task_show', { 'id' : task.id }, false, 'Back to the timeline'|trans ) }}" class="sc-button bt-show"> | ||||
|           {{ "View the task"|trans }} | ||||
|         </a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="{{ chill_path_add_return_path('chill_task_single_task_edit', { 'id' : task.id }, false, 'Back to the timeline'|trans) }}" class="sc-button bt-update"> | ||||
|           {{ "Edit task"|trans }} | ||||
|         </a> | ||||
|       </li> | ||||
|     </ul> | ||||
|          | ||||
| </div> | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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' => [], | ||||
|         ]); | ||||
|  | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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é <julien.fastre@champs-libres.coop> | ||||
|  */ | ||||
| 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 | ||||
|             ] | ||||
|         ]; | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user