mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Merge branch 'issue719_filter_activities_version_2' into testing
This commit is contained in:
		
							
								
								
									
										5
									
								
								.changes/unreleased/DX-20230623-122408.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/DX-20230623-122408.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: DX | ||||
| body: '[FilterOrderHelper] add entity choice and singleCheckbox' | ||||
| time: 2023-06-23T12:24:08.133491895+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20230623-122530.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20230623-122530.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Feature | ||||
| body: '[activity list] add filtering for activities list' | ||||
| time: 2023-06-23T12:25:30.49643551+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20230623-122702.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20230623-122702.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: '[activity list] in person context, show also the activities from the accompanying | ||||
|   periods where the person participates' | ||||
| time: 2023-06-23T12:27:02.159041095+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20230623-124438.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20230623-124438.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Feature | ||||
| body: '[activity list] add pagination to the list of activities' | ||||
| time: 2023-06-23T12:44:38.879098862+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20230621-132851.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20230621-132851.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: '[Accompanying period comments]: order comments from the most recent to the | ||||
|   oldest, in the list' | ||||
| time: 2023-06-21T13:28:51.282714011+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										5
									
								
								.changes/unreleased/Fixed-20230621-135912.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Fixed-20230621-135912.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Fixed | ||||
| body: 'Api: filter social action to keep only the currently activated' | ||||
| time: 2023-06-21T13:59:12.57760217+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										5
									
								
								.changes/unreleased/Fixed-20230621-141828.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Fixed-20230621-141828.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Fixed | ||||
| body: Fix deletion and re-creation of filiation relationship | ||||
| time: 2023-06-21T14:18:28.437876316+02:00 | ||||
| custom: | ||||
|   Issue: "82" | ||||
							
								
								
									
										3
									
								
								.changes/v2.2.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.2.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.2.1 - 2023-06-19 | ||||
| ### Fixed | ||||
| * ([#114](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/114)) [notification on document evaluation] fix entityId and return path when adding a notification on a document in an evaluation | ||||
| @@ -6,7 +6,7 @@ versionExt: md | ||||
| versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' | ||||
| kindFormat: '### {{.Kind}}' | ||||
| changeFormat: >- | ||||
|     * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{- end }}{{.Body}} | ||||
|     * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} | ||||
| custom: | ||||
|     -   key: Issue | ||||
|         label: Issue number (on chill-bundles repository) (optional) | ||||
|   | ||||
| @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v2.2.1 - 2023-06-19 | ||||
| ### Fixed | ||||
| * ([#114](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/114)) [notification on document evaluation] fix entityId and return path when adding a notification on a document in an evaluation | ||||
|  | ||||
| ## v2.2.0 - 2023-06-18 | ||||
| ### Feature | ||||
| * When navigating from a workflow regarding to an evaluation's document to an accompanying course, scroll directly to the document, and blink to highlight this document | ||||
|   | ||||
| @@ -18,11 +18,17 @@ use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; | ||||
| use Chill\ActivityBundle\Repository\ActivityUserJobRepository; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Repository\LocationRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Privacy\PrivacyEvent; | ||||
| @@ -47,68 +53,26 @@ use function array_key_exists; | ||||
|  | ||||
| final class ActivityController extends AbstractController | ||||
| { | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|  | ||||
|     private ActivityACLAwareRepositoryInterface $activityACLAwareRepository; | ||||
|  | ||||
|     private ActivityRepository $activityRepository; | ||||
|  | ||||
|     private ActivityTypeCategoryRepository $activityTypeCategoryRepository; | ||||
|  | ||||
|     private ActivityTypeRepositoryInterface $activityTypeRepository; | ||||
|  | ||||
|     private CenterResolverManagerInterface $centerResolver; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private EventDispatcherInterface $eventDispatcher; | ||||
|  | ||||
|     private LocationRepository $locationRepository; | ||||
|  | ||||
|     private LoggerInterface $logger; | ||||
|  | ||||
|     private PersonRepository $personRepository; | ||||
|  | ||||
|     private SerializerInterface $serializer; | ||||
|  | ||||
|     private ThirdPartyRepository $thirdPartyRepository; | ||||
|  | ||||
|     private TranslatorInterface $translator; | ||||
|  | ||||
|     private UserRepositoryInterface $userRepository; | ||||
|  | ||||
|     public function __construct( | ||||
|         ActivityACLAwareRepositoryInterface $activityACLAwareRepository, | ||||
|         ActivityTypeRepositoryInterface $activityTypeRepository, | ||||
|         ActivityTypeCategoryRepository $activityTypeCategoryRepository, | ||||
|         PersonRepository $personRepository, | ||||
|         ThirdPartyRepository $thirdPartyRepository, | ||||
|         LocationRepository $locationRepository, | ||||
|         ActivityRepository $activityRepository, | ||||
|         AccompanyingPeriodRepository $accompanyingPeriodRepository, | ||||
|         EntityManagerInterface $entityManager, | ||||
|         EventDispatcherInterface $eventDispatcher, | ||||
|         LoggerInterface $logger, | ||||
|         SerializerInterface $serializer, | ||||
|         UserRepositoryInterface $userRepository, | ||||
|         CenterResolverManagerInterface $centerResolver, | ||||
|         TranslatorInterface $translator | ||||
|         private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository, | ||||
|         private readonly ActivityTypeRepositoryInterface $activityTypeRepository, | ||||
|         private readonly ActivityTypeCategoryRepository $activityTypeCategoryRepository, | ||||
|         private readonly PersonRepository $personRepository, | ||||
|         private readonly ThirdPartyRepository $thirdPartyRepository, | ||||
|         private readonly LocationRepository $locationRepository, | ||||
|         private readonly ActivityRepository $activityRepository, | ||||
|         private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly EventDispatcherInterface $eventDispatcher, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly SerializerInterface $serializer, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|         private readonly CenterResolverManagerInterface $centerResolver, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|         private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|     ) { | ||||
|         $this->activityACLAwareRepository = $activityACLAwareRepository; | ||||
|         $this->activityTypeRepository = $activityTypeRepository; | ||||
|         $this->activityTypeCategoryRepository = $activityTypeCategoryRepository; | ||||
|         $this->personRepository = $personRepository; | ||||
|         $this->thirdPartyRepository = $thirdPartyRepository; | ||||
|         $this->locationRepository = $locationRepository; | ||||
|         $this->activityRepository = $activityRepository; | ||||
|         $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; | ||||
|         $this->entityManager = $entityManager; | ||||
|         $this->eventDispatcher = $eventDispatcher; | ||||
|         $this->logger = $logger; | ||||
|         $this->serializer = $serializer; | ||||
|         $this->userRepository = $userRepository; | ||||
|         $this->centerResolver = $centerResolver; | ||||
|         $this->translator = $translator; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -289,14 +253,31 @@ final class ActivityController extends AbstractController | ||||
|     { | ||||
|         $view = null; | ||||
|         $activities = []; | ||||
|         // TODO: add pagination | ||||
|  | ||||
|         [$person, $accompanyingPeriod] = $this->getEntity($request); | ||||
|         $filter = $this->buildFilterOrder($person ?? $accompanyingPeriod); | ||||
|  | ||||
|         $filterArgs = [ | ||||
|             'my_activities' => $filter->getSingleCheckboxData('my_activities'), | ||||
|             'types' => $filter->getEntityChoiceData('activity_types'), | ||||
|             'jobs' => $filter->getEntityChoiceData('jobs'), | ||||
|             'before' => $filter->getDateRangeData('activity_date')['to'], | ||||
|             'after' => $filter->getDateRangeData('activity_date')['from'], | ||||
|         ]; | ||||
|  | ||||
|         if ($person instanceof Person) { | ||||
|             $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person); | ||||
|             $count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs); | ||||
|             $paginator = $this->paginatorFactory->create($count); | ||||
|             $activities = $this->activityACLAwareRepository | ||||
|                 ->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); | ||||
|                 ->findByPerson( | ||||
|                     $person, | ||||
|                     ActivityVoter::SEE, | ||||
|                     $paginator->getCurrentPageFirstItemNumber(), | ||||
|                     $paginator->getItemsPerPage(), | ||||
|                     ['date' => 'DESC', 'id' => 'DESC'], | ||||
|                     $filterArgs | ||||
|                 ); | ||||
|  | ||||
|             $event = new PrivacyEvent($person, [ | ||||
|                 'element_class' => Activity::class, | ||||
| @@ -308,10 +289,21 @@ final class ActivityController extends AbstractController | ||||
|         } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { | ||||
|             $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); | ||||
|  | ||||
|             $count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs); | ||||
|             $paginator = $this->paginatorFactory->create($count); | ||||
|             $activities = $this->activityACLAwareRepository | ||||
|                 ->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); | ||||
|                 ->findByAccompanyingPeriod( | ||||
|                     $accompanyingPeriod, | ||||
|                     ActivityVoter::SEE, | ||||
|                     $paginator->getCurrentPageFirstItemNumber(), | ||||
|                     $paginator->getItemsPerPage(), | ||||
|                     ['date' => 'DESC', 'id' => 'DESC'], | ||||
|                     $filterArgs | ||||
|                 ); | ||||
|  | ||||
|             $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; | ||||
|         } else { | ||||
|             throw new \LogicException("Unsupported"); | ||||
|         } | ||||
|  | ||||
|         return $this->render( | ||||
| @@ -320,10 +312,40 @@ final class ActivityController extends AbstractController | ||||
|                 'activities' => $activities, | ||||
|                 'person' => $person, | ||||
|                 'accompanyingCourse' => $accompanyingPeriod, | ||||
|                 'filter' => $filter, | ||||
|                 'paginator' => $paginator, | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function buildFilterOrder(AccompanyingPeriod|Person $associated): FilterOrderHelper | ||||
|     { | ||||
|  | ||||
|         $filterBuilder = $this->filterOrderHelperFactory->create(self::class); | ||||
|         $types = $this->activityACLAwareRepository->findActivityTypeByAssociated($associated); | ||||
|         $jobs = $this->activityACLAwareRepository->findUserJobByAssociated($associated); | ||||
|  | ||||
|         $filterBuilder | ||||
|             ->addDateRange('activity_date', 'activity.date') | ||||
|             ->addSingleCheckbox('my_activities', 'activity_filter.My activities') | ||||
|             ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ | ||||
|                 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { | ||||
|                     $text = match ($activityType->hasCategory()) { | ||||
|                         true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', | ||||
|                         false => '', | ||||
|                     }; | ||||
|  | ||||
|                     return $text . $this->translatableStringHelper->localize($activityType->getName()); | ||||
|                 } | ||||
|             ]) | ||||
|             ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ | ||||
|                 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) | ||||
|             ]) | ||||
|         ; | ||||
|  | ||||
|         return $filterBuilder->build(); | ||||
|     } | ||||
|  | ||||
|     public function newAction(Request $request): Response | ||||
|     { | ||||
|         $view = null; | ||||
|   | ||||
| @@ -18,67 +18,193 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Location; | ||||
| use Chill\MainBundle\Entity\LocationType; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelper; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\DBAL\Types\Types; | ||||
| use Doctrine\ORM\AbstractQuery; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\NonUniqueResultException; | ||||
| use Doctrine\ORM\NoResultException; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\Query\ResultSetMappingBuilder; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | ||||
| use Symfony\Component\Security\Core\Role\Role; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| use function count; | ||||
| use function in_array; | ||||
|  | ||||
| final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface | ||||
| final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface | ||||
| { | ||||
|     private AuthorizationHelper $authorizationHelper; | ||||
|  | ||||
|     private CenterResolverDispatcherInterface $centerResolverDispatcher; | ||||
|  | ||||
|     private EntityManagerInterface $em; | ||||
|  | ||||
|     private ActivityRepository $repository; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private TokenStorageInterface $tokenStorage; | ||||
|  | ||||
|     public function __construct( | ||||
|         AuthorizationHelper $authorizationHelper, | ||||
|         CenterResolverDispatcherInterface $centerResolverDispatcher, | ||||
|         TokenStorageInterface $tokenStorage, | ||||
|         ActivityRepository $repository, | ||||
|         EntityManagerInterface $em, | ||||
|         Security $security | ||||
|         private AuthorizationHelperForCurrentUserInterface $authorizationHelper, | ||||
|         private CenterResolverManagerInterface $centerResolverManager, | ||||
|         private ActivityRepository $repository, | ||||
|         private EntityManagerInterface $em, | ||||
|         private Security $security, | ||||
|         private RequestStack $requestStack, | ||||
|     ) { | ||||
|         $this->authorizationHelper = $authorizationHelper; | ||||
|         $this->centerResolverDispatcher = $centerResolverDispatcher; | ||||
|         $this->tokenStorage = $tokenStorage; | ||||
|         $this->repository = $repository; | ||||
|         $this->em = $em; | ||||
|         $this->security = $security; | ||||
|     } | ||||
|  | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array | ||||
|     /** | ||||
|      * @throws NonUniqueResultException | ||||
|      * @throws NoResultException | ||||
|      */ | ||||
|     public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int | ||||
|     { | ||||
|         $user = $this->security->getUser(); | ||||
|         $center = $this->centerResolverDispatcher->resolveCenter($period); | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         if (0 === count($orderBy)) { | ||||
|             $orderBy = ['date' => 'DESC']; | ||||
|         $qb | ||||
|             ->select('COUNT(a)') | ||||
|             ->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); | ||||
|  | ||||
|         return $qb->getQuery()->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     public function countByPerson(Person $person, string $role, array $filters = []): int | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         $qb = $this->filterBaseQueryByPerson($qb, $person, $role); | ||||
|  | ||||
|         $qb->select('COUNT(a)'); | ||||
|  | ||||
|         return $qb->getQuery()->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         $qb->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); | ||||
|  | ||||
|         foreach ($orderBy as $field => $order) { | ||||
|             $qb->addOrderBy('a.' . $field, $order); | ||||
|         } | ||||
|  | ||||
|         $scopes = $this->authorizationHelper | ||||
|             ->getReachableCircles($user, $role, $center); | ||||
|         if (null !== $start) { | ||||
|             $qb->setFirstResult($start); | ||||
|         } | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         return $this->em->getRepository(Activity::class) | ||||
|             ->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy); | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function buildBaseQuery(array $filters): QueryBuilder | ||||
|     { | ||||
|         $qb = $this->repository | ||||
|             ->createQueryBuilder('a') | ||||
|         ; | ||||
|  | ||||
|         if (($filters['my_activities'] ?? false) and ($user = $this->security->getUser()) instanceof User) { | ||||
|             $qb->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     'a.createdBy = :user', | ||||
|                     'a.user = :user', | ||||
|                     ':user MEMBER OF a.users' | ||||
|                 ) | ||||
|             )->setParameter('user', $user); | ||||
|         } | ||||
|  | ||||
|         if ([] !== ($types = $filters['types'] ?? [])) { | ||||
|             $qb->andWhere('a.activityType IN (:types)')->setParameter('types', $types); | ||||
|         } | ||||
|  | ||||
|         if ([] !== ($jobs = $filters['jobs'] ?? [])) { | ||||
|             $qb | ||||
|                 ->leftJoin('a.createdBy', 'creator') | ||||
|                 ->leftJoin('a.user', 'activity_u') | ||||
|                 ->andWhere( | ||||
|                     $qb->expr()->orX( | ||||
|                         'creator.userJob IN (:jobs)', | ||||
|                         'activity_u.userJob IN (:jobs)', | ||||
|                         'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))' | ||||
|                     ) | ||||
|                 ) | ||||
|                 ->setParameter('jobs', $jobs); | ||||
|         } | ||||
|  | ||||
|         if (null !== ($after = $filters['after'] ?? null)) { | ||||
|             $qb->andWhere('a.date >= :after')->setParameter('after', $after); | ||||
|         } | ||||
|  | ||||
|         if (null !== ($before = $filters['before'] ?? null)) { | ||||
|             $qb->andWhere('a.date <= :before')->setParameter('before', $before); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param AccompanyingPeriod|Person $associated | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array | ||||
|     { | ||||
|         $in = $this->em->createQueryBuilder(); | ||||
|         $in | ||||
|             ->select('1') | ||||
|             ->from(Activity::class, 'a'); | ||||
|  | ||||
|         if ($associated instanceof Person) { | ||||
|             $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); | ||||
|         } else { | ||||
|             $in->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $associated); | ||||
|         } | ||||
|  | ||||
|         // join between the embedded exist query and the main query | ||||
|         $in->andWhere('a.activityType = t'); | ||||
|  | ||||
|         $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); | ||||
|         $qb | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->where( | ||||
|                 $qb->expr()->exists($in->getDQL()) | ||||
|             ); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array | ||||
|     { | ||||
|         $in = $this->em->createQueryBuilder(); | ||||
|         $in->select('IDENTITY(u.userJob)') | ||||
|             ->from(User::class, 'u') | ||||
|             ->join( | ||||
|                 Activity::class, | ||||
|                 'a', | ||||
|                 Join::WITH, | ||||
|                 'a.createdBy = u OR a.user = u OR u MEMBER OF a.users' | ||||
|             ); | ||||
|  | ||||
|         if ($associated instanceof Person) { | ||||
|             $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); | ||||
|         } else { | ||||
|             $in->andWhere('a.accompanyingPeriod = :associated'); | ||||
|             $in->setParameter('associated', $associated); | ||||
|         } | ||||
|  | ||||
|         $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); | ||||
|  | ||||
|         $qb->select('ub', 'JSON_EXTRACT(ub.label, :lang) AS HIDDEN lang') | ||||
|             ->from(UserJob::class, 'ub') | ||||
|             ->where($qb->expr()->in('ub.id', $in->getDQL())) | ||||
|             ->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale()) | ||||
|             ->orderBy('lang') | ||||
|         ; | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array | ||||
|     { | ||||
|         $rsm = new ResultSetMappingBuilder($this->em); | ||||
| @@ -159,25 +285,73 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|         return $nq->getResult(AbstractQuery::HYDRATE_ARRAY); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array $orderBy | ||||
|      * | ||||
|      * @return Activity[]|array | ||||
|      */ | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): array | ||||
|     { | ||||
|         $user = $this->security->getUser(); | ||||
|         $center = $this->centerResolverDispatcher->resolveCenter($person); | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         if (0 === count($orderBy)) { | ||||
|             $orderBy = ['date' => 'DESC']; | ||||
|         $qb = $this->filterBaseQueryByPerson($qb, $person, $role); | ||||
|  | ||||
|         foreach ($orderBy as $field => $direction) { | ||||
|             $qb->addOrderBy('a.' . $field, $direction); | ||||
|         } | ||||
|  | ||||
|         $reachableScopes = $this->authorizationHelper | ||||
|             ->getReachableCircles($user, $role, $center); | ||||
|         if (null !== $start) { | ||||
|             $qb->setFirstResult($start); | ||||
|         } | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         return $this->em->getRepository(Activity::class) | ||||
|             ->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start); | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     private function filterBaseQueryByPerson(QueryBuilder $qb, Person $person, string $role): QueryBuilder | ||||
|     { | ||||
|         $orX = $qb->expr()->orX(); | ||||
|         $counter = 0; | ||||
|         foreach ($this->centerResolverManager->resolveCenters($person) as $center) { | ||||
|             $scopes = $this->authorizationHelper->getReachableScopes($role, $center); | ||||
|  | ||||
|             if ([] === $scopes) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $orX->add(sprintf('a.person = :person AND a.scope IN (:scopes_%d)', $counter)); | ||||
|             $qb->setParameter(sprintf('scopes_%d', $counter), $scopes); | ||||
|             $qb->setParameter('person', $person); | ||||
|             $counter++; | ||||
|         } | ||||
|  | ||||
|         foreach  ($person->getAccompanyingPeriodParticipations() as $participation) { | ||||
|             if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $and = $qb->expr()->andX( | ||||
|                 sprintf('a.accompanyingPeriod = :period_%d', $counter), | ||||
|                 sprintf('a.date >= :participation_start_%d', $counter) | ||||
|             ); | ||||
|  | ||||
|             $qb | ||||
|                 ->setParameter(sprintf('period_%d', $counter), $participation->getAccompanyingPeriod()) | ||||
|                 ->setParameter(sprintf('participation_start_%d', $counter), $participation->getStartDate()); | ||||
|  | ||||
|             if (null !== $participation->getEndDate()) { | ||||
|                 $and->add(sprintf('a.date < :participation_end_%d', $counter)); | ||||
|                 $qb | ||||
|                     ->setParameter(sprintf('participation_end_%d', $counter), $participation->getEndDate()); | ||||
|             } | ||||
|             $orX->add($and); | ||||
|             $counter++; | ||||
|         } | ||||
|  | ||||
|         if (0 === $orX->count()) { | ||||
|             $qb->andWhere('FALSE = TRUE'); | ||||
|         } else { | ||||
|             $qb->andWhere($orX); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function queryTimelineIndexer(string $context, array $args = []): array | ||||
| @@ -226,7 +400,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|  | ||||
|         // acls: | ||||
|         $reachableCenters = $this->authorizationHelper->getReachableCenters( | ||||
|             $this->tokenStorage->getToken()->getUser(), | ||||
|             ActivityVoter::SEE | ||||
|         ); | ||||
|  | ||||
| @@ -251,7 +424,7 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|                 continue; | ||||
|             } | ||||
|             // we get all the reachable scopes for this center | ||||
|             $reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE, $center); | ||||
|             $reachableScopes = $this->authorizationHelper->getReachableScopes(ActivityVoter::SEE, $center); | ||||
|             // we get the ids for those scopes | ||||
|             $reachablesScopesId = array_map( | ||||
|                 static fn (Scope $scope) => $scope->getId(), | ||||
|   | ||||
| @@ -11,15 +11,32 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
|  | ||||
| interface ActivityACLAwareRepositoryInterface | ||||
| { | ||||
|     /** | ||||
|      * @return Activity[]|array | ||||
|      * Return all the activities associated to an accompanying period and that the user is allowed to apply the given role. | ||||
|      * | ||||
|      * | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      * @return array<Activity> | ||||
|      */ | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; | ||||
|  | ||||
|     /** | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      */ | ||||
|     public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int; | ||||
|  | ||||
|     /** | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      */ | ||||
|     public function countByPerson(Person $person, string $role, array $filters = []): int; | ||||
|  | ||||
|     /** | ||||
|      * Return a list of activities, simplified as array (not object). | ||||
| @@ -31,7 +48,28 @@ interface ActivityACLAwareRepositoryInterface | ||||
|     public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array; | ||||
|  | ||||
|     /** | ||||
|      * @return Activity[]|array | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      * @return array<Activity> | ||||
|      */ | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Return a list of the type for the activities associated to person or accompanying period | ||||
|      * | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array; | ||||
|  | ||||
|     /** | ||||
|      * Return a list of the user job for the activities associated to person or accompanying period | ||||
|      * | ||||
|      * Associated mean the job: | ||||
|      * - of the creator; | ||||
|      * - of the user (activity.user) | ||||
|      * - of all the users | ||||
|      * | ||||
|      * @return array<UserJob> | ||||
|      */ | ||||
|     public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array; | ||||
| } | ||||
|   | ||||
| @@ -11,9 +11,13 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
|  | ||||
| final class ActivityTypeRepository implements ActivityTypeRepositoryInterface | ||||
| { | ||||
|   | ||||
| @@ -12,12 +12,14 @@ declare(strict_types=1); | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| interface ActivityTypeRepositoryInterface extends ObjectRepository | ||||
| { | ||||
|     /** | ||||
|      * @return array|ActivityType[] | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findAllActive(): array; | ||||
| } | ||||
|   | ||||
| @@ -80,12 +80,15 @@ | ||||
|  | ||||
| <div class="context-{{ context }}"> | ||||
|  | ||||
|     {{ filter|chill_render_filter_order_helper }} | ||||
|  | ||||
|     {% if activities|length == 0 %} | ||||
|         <p class="chill-no-data-statement"> | ||||
|             {{ "There isn't any activities."|trans }} | ||||
|         </p> | ||||
|  | ||||
|     {% else %} | ||||
|  | ||||
|         <div class="flex-table activity-list"> | ||||
|             {% for activity in activities %} | ||||
|                 {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with { | ||||
| @@ -96,4 +99,6 @@ | ||||
|         </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {{ chill_pagination(paginator) }} | ||||
|  | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,325 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\ActivityBundle\Tests\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\ActivityBundle\Repository\ActivityACLAwareRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Center; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|     private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser; | ||||
|  | ||||
|     private CenterResolverManagerInterface $centerResolverManager; | ||||
|  | ||||
|     private ActivityRepository $activityRepository; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private RequestStack $requestStack; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|  | ||||
|         $this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); | ||||
|         $this->activityRepository = self::$container->get(ActivityRepository::class); | ||||
|         $this->entityManager = self::$container->get(EntityManagerInterface::class); | ||||
|         $this->security = self::$container->get(Security::class); | ||||
|  | ||||
|         $this->requestStack = $requestStack = new RequestStack(); | ||||
|         $request = $this->prophesize(Request::class); | ||||
|         $request->getLocale()->willReturn('fr'); | ||||
|         $request->getDefaultLocale()->willReturn('fr'); | ||||
|         $requestStack->push($request->reveal()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testFindByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByAccompanyingPeriod($period, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testFindActivityTypeByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findActivityTypeByAssociated($period); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByPerson | ||||
|      */ | ||||
|     public function testFindActivityTypeByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void | ||||
|     { | ||||
|         $role = ActivityVoter::SEE; | ||||
|         $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); | ||||
|         $centerResolver->resolveCenters($person)->willReturn($centers); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) | ||||
|             ->willReturn($scopes); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $authorizationHelper->reveal(), | ||||
|             $centerResolver->reveal(), | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByPerson | ||||
|      */ | ||||
|     public function testFindByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void | ||||
|     { | ||||
|         $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); | ||||
|         $centerResolver->resolveCenters($person)->willReturn($centers); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) | ||||
|             ->willReturn($scopes); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $authorizationHelper->reveal(), | ||||
|             $centerResolver->reveal(), | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByPerson(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         /** @var Person $person */ | ||||
|         if (null === $person = $this->entityManager->createQueryBuilder() | ||||
|             ->select('p')->from(Person::class, 'p')->setMaxResults(1) | ||||
|             ->getQuery()->getSingleResult()) { | ||||
|             throw new \RuntimeException("person not found"); | ||||
|         } | ||||
|  | ||||
|         /** @var AccompanyingPeriod $period1 */ | ||||
|         if (null === $period1 = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no period found"); | ||||
|         } | ||||
|  | ||||
|         /** @var AccompanyingPeriod $period2 */ | ||||
|         if (null === $period2 = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->where('a.id > :pid') | ||||
|             ->setParameter('pid', $period1->getId()) | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no second period found"); | ||||
|         } | ||||
|         // add a period | ||||
|         $period1->addPerson($person); | ||||
|         $period2->addPerson($person); | ||||
|         $period1->getParticipationsContainsPerson($person)->first()->setEndDate( | ||||
|             (new \DateTime('now'))->add(new \DateInterval('P1M')) | ||||
|         ); | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no types"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no jobs found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $centers = $this->entityManager->createQueryBuilder() | ||||
|             ->select('c')->from(Center::class, 'c')->setMaxResults(2)->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no centers found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $scopes = $this->entityManager->createQueryBuilder() | ||||
|             ->select('s')->from(Scope::class, 's')->setMaxResults(2)->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no scopes found"); | ||||
|         } | ||||
|  | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], []]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $period = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no period found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no types"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no jobs found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
| } | ||||
| @@ -83,12 +83,20 @@ Third persons: Tiers non-pro. | ||||
| Others persons: Usagers | ||||
| Third parties: Tiers professionnels | ||||
| Users concerned: T(M)S | ||||
|  | ||||
| activity: | ||||
|     date: Date de l'échange | ||||
|     Insert a document: Insérer un document | ||||
|     Remove a document: Supprimer le document | ||||
|     comment: Commentaire | ||||
| No documents: Aucun document | ||||
|  | ||||
| # activity filter in list page | ||||
| activity_filter: | ||||
|     My activities: Mes échanges (où j'interviens) | ||||
|     Types: Par type d'échange | ||||
|     Jobs: Par métier impliqué | ||||
|  | ||||
| #timeline | ||||
| '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class UserJob | ||||
|     protected ?int $id = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array|string[]A | ||||
|      * @var array<string, string> | ||||
|      * @ORM\Column(name="label", type="json") | ||||
|      * @Serializer\Groups({"read", "docgen:read"}) | ||||
|      * @Serializer\Context({"is-translatable": true}, groups={"docgen:read"}) | ||||
|   | ||||
| @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Form\Type\Listing; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\ChillDateType; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| use Symfony\Bridge\Doctrine\Form\Type\EntityType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\HiddenType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SearchType; | ||||
| @@ -27,13 +29,6 @@ use function count; | ||||
|  | ||||
| final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
| { | ||||
|     private RequestStack $requestStack; | ||||
|  | ||||
|     public function __construct(RequestStack $requestStack) | ||||
|     { | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         /** @var FilterOrderHelper $helper */ | ||||
| @@ -71,6 +66,25 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
|             $builder->add($checkboxesBuilder); | ||||
|         } | ||||
|  | ||||
|         if ([] !== $helper->getEntityChoices()) { | ||||
|             $entityChoicesBuilder = $builder->create('entity_choices', null, ['compound' => true]); | ||||
|  | ||||
|             foreach ($helper->getEntityChoices() as $key => [ | ||||
|                 'label' => $label, 'choices' => $choices, 'options' => $opts, 'class' => $class | ||||
|             ]) { | ||||
|                 $entityChoicesBuilder->add($key, EntityType::class, [ | ||||
|                     'label' => $label, | ||||
|                     'choices' => $choices, | ||||
|                     'class' => $class, | ||||
|                     'multiple' => true, | ||||
|                     'expanded' => true, | ||||
|                     ...$opts, | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             $builder->add($entityChoicesBuilder); | ||||
|         } | ||||
|  | ||||
|         if (0 < count($helper->getDateRanges())) { | ||||
|             $dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]); | ||||
|  | ||||
| @@ -97,28 +111,14 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
|             $builder->add($dateRangesBuilder); | ||||
|         } | ||||
|  | ||||
|         foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { | ||||
|             switch ($key) { | ||||
|                 case 'q': | ||||
|                 case 'checkboxes' . $key: | ||||
|                 case $key . '_from': | ||||
|                 case $key . '_to': | ||||
|                     break; | ||||
|         if ([] !== $helper->getSingleCheckbox()) { | ||||
|             $singleCheckBoxBuilder = $builder->create('single_checkboxes', null, ['compound' => true]); | ||||
|  | ||||
|                 case 'page': | ||||
|                     $builder->add($key, HiddenType::class, [ | ||||
|                         'data' => 1, | ||||
|                     ]); | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     $builder->add($key, HiddenType::class, [ | ||||
|                         'data' => $value, | ||||
|                     ]); | ||||
|  | ||||
|                     break; | ||||
|             foreach ($helper->getSingleCheckbox() as $name => ['label' => $label]) { | ||||
|                 $singleCheckBoxBuilder->add($name, CheckboxType::class, ['label' => $label, 'required' => false]); | ||||
|             } | ||||
|  | ||||
|             $builder->add($singleCheckBoxBuilder); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| const buildLinkCreate = function (relatedEntityClass: string, relatedEntityId: number, to: number | null, returnPath: string | null): string | ||||
| { | ||||
|     const params = new URLSearchParams(); | ||||
|     params.append('entityClass', relatedEntityClass); | ||||
|     params.append('entityId', relatedEntityId.toString()); | ||||
|  | ||||
|     if (null !== to) { | ||||
|         params.append('tos[0]', to.toString()); | ||||
|     } | ||||
|  | ||||
|     if (null !== returnPath) { | ||||
|         params.append('returnPath', returnPath); | ||||
|     } | ||||
|  | ||||
|     return `/fr/notification/create?${params.toString()}`; | ||||
| } | ||||
|  | ||||
| export { | ||||
|     buildLinkCreate, | ||||
| } | ||||
| @@ -13,13 +13,13 @@ | ||||
|         {% if form.dateRanges is defined %} | ||||
|             {% if form.dateRanges|length > 0 %} | ||||
|                 {% for dateRangeName, _o in form.dateRanges %} | ||||
|                     <div class="row gx-2 justify-content-center"> | ||||
|                     <div class="row gx-2 justify-content-center items-center"> | ||||
|                         {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} | ||||
|                         <div class="col-md-5"> | ||||
|                         <div class="col-md-4"> | ||||
|                             {{ form_label(form.dateRanges[dateRangeName])}} | ||||
|                         </div> | ||||
|                         {% endif %} | ||||
|                         <div class="col-md-6"> | ||||
|                         <div class="col-md-7"> | ||||
|                             <div class="input-group mb-3"> | ||||
|                                 <span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span> | ||||
|                                 {{ form_widget(form.dateRanges[dateRangeName]['from']) }} | ||||
| @@ -27,7 +27,7 @@ | ||||
|                                 {{ form_widget(form.dateRanges[dateRangeName]['to']) }} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-md-1"> | ||||
|                         <div class="col-md-1" style="text-align: right;"> | ||||
|                             <button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button> | ||||
|                         </div> | ||||
|                     </div> | ||||
| @@ -37,7 +37,7 @@ | ||||
|         {% if form.checkboxes is defined %} | ||||
|             {% if form.checkboxes|length > 0 %} | ||||
|                 {% for checkbox_name, options in form.checkboxes %} | ||||
|                     <div class="row gx-0"> | ||||
|                     <div class="row gx-0 align-items-center"> | ||||
|                         <div class="col-md-12"> | ||||
|                             {% for c in form['checkboxes'][checkbox_name].children %} | ||||
|                                 <div class="form-check form-check-inline"> | ||||
| @@ -61,5 +61,45 @@ | ||||
|                 {% endfor %} | ||||
|             {% endif %} | ||||
|         {% endif %} | ||||
|         {% if form.entity_choices is defined %} | ||||
|             {% if form.entity_choices |length > 0 %} | ||||
|                 {% for checkbox_name, options in form.entity_choices %} | ||||
|                     <div class="row gx-0 align-items-center"> | ||||
|                         {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} | ||||
|                             <div class="col-md-4"> | ||||
|                                 {{ form_label(form.entity_choices[checkbox_name])}} | ||||
|                             </div> | ||||
|                         {% endif %} | ||||
|                         <div class="col-md-7"> | ||||
|                             {% for c in form['entity_choices'][checkbox_name].children %} | ||||
|                                 <div class="form-check form-check-inline"> | ||||
|                                     {{ form_widget(c) }} | ||||
|                                     {{ form_label(c) }} | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|                         </div> | ||||
|                         <div class="col-md-1 text-right" style="text-align: right;"> | ||||
|                             <button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endfor %} | ||||
|             {% endif %} | ||||
|         {% endif %} | ||||
|         {% if form.single_checkboxes is defined %} | ||||
|             {% for name, _o in form.single_checkboxes %} | ||||
|                 <div class="row gx-2 align-items-center"> | ||||
|                     <div class="col-md-7 offset-md-4"> | ||||
|                         {{ form_widget(form.single_checkboxes[name]) }} | ||||
|                     </div> | ||||
|                     <div class="col-md-1 text-right" style="text-align: right;"> | ||||
|                         <button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             {% endfor %} | ||||
|         {% endif %} | ||||
|     </div> | ||||
|  | ||||
|     {% for k,v in otherParameters %} | ||||
|         <input type="hidden" name="{{ k }}" value="{{ v }}" /> | ||||
|     {% endfor %} | ||||
| {{ form_end(form) }} | ||||
|   | ||||
| @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\Listing\FilterOrderType; | ||||
| use DateTimeImmutable; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\HiddenType; | ||||
| use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| @@ -24,11 +26,16 @@ class FilterOrderHelper | ||||
| { | ||||
|     private array $checkboxes = []; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string}> | ||||
|      */ | ||||
|     private array $singleCheckbox = []; | ||||
|  | ||||
|     private array $dateRanges = []; | ||||
|  | ||||
|     private FormFactoryInterface $formFactory; | ||||
|  | ||||
|     private ?string $formName = 'f'; | ||||
|     public const FORM_NAME = 'f'; | ||||
|  | ||||
|     private array $formOptions = []; | ||||
|  | ||||
| @@ -40,6 +47,11 @@ class FilterOrderHelper | ||||
|  | ||||
|     private ?array $submitted = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string, choices: array, options: array}> | ||||
|      */ | ||||
|     private array $entityChoices = []; | ||||
|  | ||||
|     public function __construct( | ||||
|         FormFactoryInterface $formFactory, | ||||
|         RequestStack $requestStack | ||||
| @@ -48,7 +60,29 @@ class FilterOrderHelper | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self | ||||
|     public function addSingleCheckbox(string $name, string $label): self | ||||
|     { | ||||
|         $this->singleCheckbox[$name] = ['label' => $label]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param class-string $class | ||||
|      */ | ||||
|     public function addEntityChoice(string $name, string $class, string $label, array $choices, array $options = []): self | ||||
|     { | ||||
|         $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getEntityChoices(): array | ||||
|     { | ||||
|         return $this->entityChoices; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self | ||||
|     { | ||||
|         $missing = count($choices) - count($trans) - 1; | ||||
|         $this->checkboxes[$name] = [ | ||||
| @@ -58,6 +92,7 @@ class FilterOrderHelper | ||||
|                 0 < $missing ? | ||||
|                        array_fill(0, $missing, null) : [] | ||||
|             ), | ||||
|             ...$options, | ||||
|         ]; | ||||
|  | ||||
|         return $this; | ||||
| @@ -73,7 +108,7 @@ class FilterOrderHelper | ||||
|     public function buildForm(): FormInterface | ||||
|     { | ||||
|         return $this->formFactory | ||||
|             ->createNamed($this->formName, $this->formType, $this->getDefaultData(), array_merge([ | ||||
|             ->createNamed(self::FORM_NAME, $this->formType, $this->getDefaultData(), array_merge([ | ||||
|                 'helper' => $this, | ||||
|                 'method' => 'GET', | ||||
|                 'csrf_protection' => false, | ||||
| @@ -86,6 +121,16 @@ class FilterOrderHelper | ||||
|         return $this->getFormData()['checkboxes'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function getSingleCheckboxData(string $name): ?bool | ||||
|     { | ||||
|         return $this->getFormData()['single_checkboxes'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function getEntityChoiceData($name): mixed | ||||
|     { | ||||
|         return $this->getFormData()['entity_choices'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function getCheckboxes(): array | ||||
|     { | ||||
|         return $this->checkboxes; | ||||
| @@ -97,7 +142,15 @@ class FilterOrderHelper | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable> | ||||
|      * @return array<string, array{label: string}> | ||||
|      */ | ||||
|     public function getSingleCheckbox(): array | ||||
|     { | ||||
|         return $this->singleCheckbox; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable} | ||||
|      */ | ||||
|     public function getDateRangeData(string $name): array | ||||
|     { | ||||
| @@ -128,7 +181,12 @@ class FilterOrderHelper | ||||
|  | ||||
|     private function getDefaultData(): array | ||||
|     { | ||||
|         $r = []; | ||||
|         $r = [ | ||||
|             'checkboxes' => [], | ||||
|             'dateRanges' => [], | ||||
|             'single_checkboxes' => [], | ||||
|             'entity_choices' => [] | ||||
|         ]; | ||||
|  | ||||
|         if ($this->hasSearchBox()) { | ||||
|             $r['q'] = ''; | ||||
| @@ -143,6 +201,14 @@ class FilterOrderHelper | ||||
|             $r['dateRanges'][$name]['to'] = $defaults['to']; | ||||
|         } | ||||
|  | ||||
|         foreach ($this->singleCheckbox as $name => $c) { | ||||
|             $r['single_checkboxes'][$name] = false; | ||||
|         } | ||||
|  | ||||
|         foreach ($this->entityChoices as $name => $c) { | ||||
|             $r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null; | ||||
|         } | ||||
|  | ||||
|         return $r; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,16 @@ class FilterOrderHelperBuilder | ||||
|  | ||||
|     private ?array $searchBoxFields = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string}> | ||||
|      */ | ||||
|     private array $singleCheckboxes = []; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string, class: class-string, choices: array, options: array}> | ||||
|      */ | ||||
|     private array $entityChoices = []; | ||||
|  | ||||
|     public function __construct( | ||||
|         FormFactoryInterface $formFactory, | ||||
|         RequestStack $requestStack | ||||
| @@ -35,6 +45,13 @@ class FilterOrderHelperBuilder | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function addSingleCheckbox(string $name, string $label): self | ||||
|     { | ||||
|         $this->singleCheckboxes[$name] = ['label' => $label]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self | ||||
|     { | ||||
|         $this->checkboxes[$name] = ['choices' => $choices, 'default' => $default, 'trans' => $trans]; | ||||
| @@ -42,6 +59,16 @@ class FilterOrderHelperBuilder | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param class-string $class | ||||
|      */ | ||||
|     public function addEntityChoice(string $name, string $label, string $class, array $choices, ?array $options = []): self | ||||
|     { | ||||
|         $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self | ||||
|     { | ||||
|         $this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label]; | ||||
| @@ -75,6 +102,18 @@ class FilterOrderHelperBuilder | ||||
|             $helper->addCheckbox($name, $choices, $default, $trans); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->singleCheckboxes as $name => ['label' => $label] | ||||
|         ) { | ||||
|             $helper->addSingleCheckbox($name, $label); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options] | ||||
|         ) { | ||||
|             $helper->addEntityChoice($name, $class, $label, $choices, $options); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->dateRanges as $name => [ | ||||
|                 'from' => $from, | ||||
|   | ||||
| @@ -11,13 +11,23 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Templating\Listing; | ||||
|  | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Twig\Environment; | ||||
| use Twig\Error\LoaderError; | ||||
| use Twig\Error\RuntimeError; | ||||
| use Twig\Error\SyntaxError; | ||||
| use Twig\Extension\AbstractExtension; | ||||
| use Twig\TwigFilter; | ||||
|  | ||||
| class Templating extends AbstractExtension | ||||
| { | ||||
|     public function getFilters() | ||||
|     public function __construct( | ||||
|         private readonly RequestStack $requestStack, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function getFilters(): array | ||||
|     { | ||||
|         return [ | ||||
|             new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [ | ||||
| @@ -26,16 +36,41 @@ class Templating extends AbstractExtension | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws SyntaxError | ||||
|      * @throws RuntimeError | ||||
|      * @throws LoaderError | ||||
|      */ | ||||
|     public function renderFilterOrderHelper( | ||||
|         Environment $environment, | ||||
|         FilterOrderHelper $helper, | ||||
|         ?string $template = '@ChillMain/FilterOrder/base.html.twig', | ||||
|         ?array $options = [] | ||||
|     ) { | ||||
|     ): string { | ||||
|         $otherParameters = []; | ||||
|  | ||||
|         foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { | ||||
|             switch ($key) { | ||||
|                 case FilterOrderHelper::FORM_NAME: | ||||
|                     break; | ||||
|  | ||||
|                 case PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY: | ||||
|                     // when filtering, go back to page 1 | ||||
|                     $otherParameters[PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY] = 1; | ||||
|  | ||||
|                     break; | ||||
|                 default: | ||||
|                     $otherParameters[$key] = $value; | ||||
|  | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $environment->render($template, [ | ||||
|             'helper' => $helper, | ||||
|             'form' => $helper->buildForm()->createView(), | ||||
|             'options' => $options, | ||||
|             'otherParameters' => $otherParameters, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,21 +16,19 @@ use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Chill\PersonBundle\Entity\SocialWork\SocialAction; | ||||
| use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| use function count; | ||||
|  | ||||
| class SocialWorkSocialActionApiController extends ApiController | ||||
| final class SocialWorkSocialActionApiController extends ApiController | ||||
| { | ||||
|     private PaginatorFactory $paginator; | ||||
|  | ||||
|     private SocialIssueRepository $socialIssueRepository; | ||||
|  | ||||
|     public function __construct(SocialIssueRepository $socialIssueRepository, PaginatorFactory $paginator) | ||||
|     { | ||||
|         $this->socialIssueRepository = $socialIssueRepository; | ||||
|         $this->paginator = $paginator; | ||||
|     public function __construct( | ||||
|         private readonly SocialIssueRepository $socialIssueRepository, | ||||
|         private readonly PaginatorFactory $paginator, | ||||
|         private readonly ClockInterface $clock, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function listBySocialIssueApi($id, Request $request) | ||||
| @@ -42,7 +40,10 @@ class SocialWorkSocialActionApiController extends ApiController | ||||
|             throw $this->createNotFoundException('socialIssue not found'); | ||||
|         } | ||||
|  | ||||
|         $socialActions = $socialIssue->getRecursiveSocialActions()->toArray(); | ||||
|         $socialActions = SocialAction::filterRemoveDeactivatedActions( | ||||
|             $socialIssue->getRecursiveSocialActions()->toArray(), | ||||
|             \DateTime::createFromImmutable($this->clock->now()) | ||||
|         ); | ||||
|  | ||||
|         usort($socialActions, static fn (SocialAction $sa, SocialAction $sb) => $sa->getOrdering() <=> $sb->getOrdering()); | ||||
|  | ||||
|   | ||||
| @@ -185,7 +185,9 @@ class AccompanyingPeriod implements | ||||
|      *     cascade={"persist", "remove"}, | ||||
|      *     orphanRemoval=true | ||||
|      * ) | ||||
|      * @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"}) | ||||
|      * @Assert\NotBlank(groups={AccompanyingPeriod::STEP_DRAFT}) | ||||
|      * @var Collection<Comment> | ||||
|      */ | ||||
|     private Collection $comments; | ||||
|  | ||||
| @@ -705,10 +707,11 @@ class AccompanyingPeriod implements | ||||
|             ->comments | ||||
|             ->filter( | ||||
|                 static fn (Comment $c): bool => $c !== $pinnedComment | ||||
|             ); | ||||
|             ) | ||||
|         ; | ||||
|     } | ||||
|  | ||||
|     public function getCreatedAt(): ?DateTime | ||||
|     public function getCreatedAt(): DateTimeInterface | ||||
|     { | ||||
|         return $this->createdAt; | ||||
|     } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use DateInterval; | ||||
| use DateTimeInterface; | ||||
| use Doctrine\Common\Collections\ArrayCollection; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\Common\Collections\ReadableCollection; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Component\Serializer\Annotation as Serializer; | ||||
|  | ||||
| @@ -295,6 +296,19 @@ class SocialAction | ||||
|         return 0 < $this->getChildren()->count(); | ||||
|     } | ||||
|  | ||||
|     public function isDesactivated(\DateTime $atDate): bool | ||||
|     { | ||||
|         if (null !== $this->desactivationDate && $this->desactivationDate < $atDate) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if ($this->hasParent()) { | ||||
|             return $this->parent->isDesactivated($atDate); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public function hasParent(): bool | ||||
|     { | ||||
|         return $this->getParent() instanceof self; | ||||
| @@ -401,4 +415,14 @@ class SocialAction | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public static function filterRemoveDeactivatedActions(ReadableCollection|array $actions, \DateTime $comparisonDate): ReadableCollection|array | ||||
|     { | ||||
|         $filterFn = fn (SocialAction $socialAction) => !$socialAction->isDesactivated($comparisonDate); | ||||
|  | ||||
|         return match ($actions instanceof ReadableCollection) { | ||||
|             true => $actions->filter($filterFn), | ||||
|             false => array_filter($actions, $filterFn) | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -253,7 +253,7 @@ class SocialIssue | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection|SocialAction[] All the descendant social actions of all | ||||
|      * @return Collection<SocialAction> All the descendant social actions of all | ||||
|      * the descendants of the entity | ||||
|      */ | ||||
|     public function getRecursiveSocialActions(): Collection | ||||
| @@ -272,7 +272,7 @@ class SocialIssue | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection|SocialAction[] | ||||
|      * @return Collection<SocialAction> | ||||
|      */ | ||||
|     public function getSocialActions(): Collection | ||||
|     { | ||||
|   | ||||
| @@ -201,6 +201,7 @@ import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload | ||||
| import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue'; | ||||
| import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue'; | ||||
| import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; | ||||
| import {buildLinkCreate as buildLinkCreateNotification} from 'ChillMainAssets/lib/entity-notification/api'; | ||||
| import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; | ||||
|  | ||||
| const i18n = { | ||||
| @@ -413,14 +414,16 @@ export default { | ||||
|          return this.$store.dispatch('submit', callback) | ||||
|             .catch(e => { console.log(e); throw e; }); | ||||
|       }, | ||||
|        goToGenerateDocumentNotification(document, tos){ | ||||
|        goToGenerateDocumentNotification(document, tos) { | ||||
|           const callback = (data) => { | ||||
|              let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key); | ||||
|              if (tos === true) { | ||||
|                 window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument&entityId=${document.id}&tos[0]=${this.$store.state.work.accompanyingPeriod.user.id}&returnPath=/fr/person/accompanying-period/work/${evaluation.id}/edit`) | ||||
|              } else { | ||||
|                 window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument&entityId=${document.id}&returnPath=/fr/person/accompanying-period/work/${evaluation.id}/edit`) | ||||
|              } | ||||
|              let updatedDocument = evaluation.documents.find(d => d.key === document.key); | ||||
|              window.location.assign(buildLinkCreateNotification( | ||||
|                  'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', | ||||
|                  updatedDocument.id, | ||||
|                  tos === true ? this.$store.state.work.accompanyingPeriod.user.id : null, | ||||
|                  window.location.pathname + window.location.search + window.location.hash | ||||
|              )); | ||||
|           }; | ||||
|            return this.$store.dispatch('submit', callback) | ||||
|                .catch(e => {console.log(e); throw e}); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|                 <td>{{ entity.ordering }}</td> | ||||
|                 <td> | ||||
|                 {% if entity.desactivationDate is not null %} | ||||
|                     {{ entity.desactivationDate|date('Y-m-d') }} | ||||
|                     {{ entity.desactivationDate|format_date('medium') }} | ||||
|                 {% endif %} | ||||
|                 </td> | ||||
|                 <td> | ||||
|   | ||||
| @@ -46,11 +46,13 @@ class RelationshipNoDuplicateValidator extends ConstraintValidator | ||||
|         ]); | ||||
|  | ||||
|         foreach ($relationships as $r) { | ||||
|             if ( | ||||
|                 $r->getFromPerson() === $fromPerson | ||||
|                 || $r->getFromPerson() === $toPerson | ||||
|                 || $r->getToPerson() === $fromPerson | ||||
|                 || $r->getToPerson() === $toPerson | ||||
|             if (spl_object_hash($r) !== spl_object_hash($value) | ||||
|                 and | ||||
|                 ( | ||||
|                     ($r->getFromPerson() === $fromPerson and $r->getToPerson() === $toPerson) | ||||
|                     || | ||||
|                     ($r->getFromPerson() === $toPerson and $r->getToPerson() === $fromPerson) | ||||
|                 ) | ||||
|             ) { | ||||
|                 $this->context->buildViolation($constraint->message) | ||||
|                     ->addViolation(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user