diff --git a/.changes/unreleased/Feature-20230623-122530.yaml b/.changes/unreleased/Feature-20230623-122530.yaml new file mode 100644 index 000000000..922750ea8 --- /dev/null +++ b/.changes/unreleased/Feature-20230623-122530.yaml @@ -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: "" diff --git a/.changes/unreleased/Feature-20230623-122702.yaml b/.changes/unreleased/Feature-20230623-122702.yaml new file mode 100644 index 000000000..e1d1b0e1f --- /dev/null +++ b/.changes/unreleased/Feature-20230623-122702.yaml @@ -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: "" diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 444c663fc..07e4fcf62 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -18,11 +18,16 @@ 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\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 +52,25 @@ 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, ) { - $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; } /** @@ -292,11 +254,27 @@ final class ActivityController extends AbstractController // 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); $activities = $this->activityACLAwareRepository - ->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); + ->findByPerson( + $person, + ActivityVoter::SEE, + 0, + null, + ['date' => 'DESC', 'id' => 'DESC'], + $filterArgs + ); $event = new PrivacyEvent($person, [ 'element_class' => Activity::class, @@ -309,7 +287,14 @@ final class ActivityController extends AbstractController $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); $activities = $this->activityACLAwareRepository - ->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); + ->findByAccompanyingPeriod( + $accompanyingPeriod, + ActivityVoter::SEE, + 0, + null, + ['date' => 'DESC', 'id' => 'DESC'], + $filterArgs + ); $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; } @@ -320,10 +305,39 @@ final class ActivityController extends AbstractController 'activities' => $activities, 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, + 'filter' => $filter, ] ); } + 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; diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 1f2039a2c..1fd0a57d6 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -18,67 +18,159 @@ 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\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 + public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array { - $user = $this->security->getUser(); - $center = $this->centerResolverDispatcher->resolveCenter($period); + $qb = $this->buildBaseQuery($filters); - if (0 === count($orderBy)) { - $orderBy = ['date' => 'DESC']; + $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); + $qb->setFirstResult(0)->setMaxResults(1000); - 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 + */ + 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 +251,66 @@ 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); + return $qb->getQuery()->getResult(); + } - return $this->em->getRepository(Activity::class) - ->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start); + 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 +359,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte // acls: $reachableCenters = $this->authorizationHelper->getReachableCenters( - $this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE ); @@ -251,7 +383,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(), diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php index 8cdb83524..a046ad218 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php @@ -11,15 +11,22 @@ 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, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + * @return array */ - 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; /** * Return a list of activities, simplified as array (not object). @@ -31,7 +38,28 @@ interface ActivityACLAwareRepositoryInterface public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array; /** - * @return Activity[]|array + * @param array{my_activities?: bool, types?: array, jobs?: array, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + * @return 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 = ['date' => 'DESC'], array $filters = []): array; + + + /** + * Return a list of the type for the activities associated to person or accompanying period + * + * @return array + */ + 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 + */ + public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array; } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php index fd5d52fce..10bd1e651 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepository.php @@ -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 { diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php index 2148a02f5..574faea22 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeRepositoryInterface.php @@ -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 */ public function findAllActive(): array; } diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index 79c946f17..17ae0598b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -86,6 +86,9 @@

{% else %} + + {{ filter|chill_render_filter_order_helper }} +
{% for activity in activities %} {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with { diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php new file mode 100644 index 000000000..e7b2117d4 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php @@ -0,0 +1,325 @@ +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')]]; + } +} diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 042fccc69..1229494bb 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -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%"'