diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bbf3ff45..b5cb4f6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,18 @@ Version 1.5.1 - [report activity count] fix error: do not show centers which are not selected in results. -`master branch` -============== +`master branch` 2 +================= - [report activity]: add aggregator for activity users - fix bug: error when extracting activities without filter / aggregators selecting persons +Master branch +============= + +- [aggregate by activity type] fix translation in aggregate activity type +- fix some translation in export +- fix error when persons not loaded by other aggregators / filters +- add "filter by activity type" filter +- add privacy events to activity list / show / edit + diff --git a/Controller/ActivityController.php b/Controller/ActivityController.php index a312bd480..8e6c25e76 100644 --- a/Controller/ActivityController.php +++ b/Controller/ActivityController.php @@ -22,11 +22,12 @@ namespace Chill\ActivityBundle\Controller; +use Chill\PersonBundle\Privacy\PrivacyEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Security\Core\Role\Role; - use Chill\ActivityBundle\Entity\Activity; use Chill\PersonBundle\Entity\Person; use Chill\ActivityBundle\Form\ActivityType; @@ -37,7 +38,22 @@ use Chill\ActivityBundle\Form\ActivityType; */ class ActivityController extends Controller { - + + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * ActivityController constructor. + * + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + } + /** * Lists all Activity entities. * @@ -62,8 +78,13 @@ class ActivityController extends Controller array('person' => $person, 'scope' => $reachableScopes), array('date' => 'DESC') ); - - + + $event = new PrivacyEvent($person, array( + 'element_class' => Activity::class, + 'action' => 'list' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + return $this->render('ChillActivityBundle:Activity:list.html.twig', array( 'activities' => $activities, 'person' => $person @@ -201,7 +222,14 @@ class ActivityController extends Controller $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity); $deleteForm = $this->createDeleteForm($id, $person); - + + $event = new PrivacyEvent($person, array( + 'element_class' => Activity::class, + 'element_id' => $entity->getId(), + 'action' => 'show' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + return $this->render('ChillActivityBundle:Activity:show.html.twig', array( 'person' => $person, 'entity' => $entity, @@ -234,6 +262,13 @@ class ActivityController extends Controller $editForm = $this->createEditForm($entity); $deleteForm = $this->createDeleteForm($id, $person); + + $event = new PrivacyEvent($person, array( + 'element_class' => Activity::class, + 'element_id' => $entity->getId(), + 'action' => 'edit' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); return $this->render('ChillActivityBundle:Activity:edit.html.twig', array( 'entity' => $entity, @@ -285,6 +320,13 @@ class ActivityController extends Controller $deleteForm = $this->createDeleteForm($id, $person); $editForm = $this->createEditForm($entity); $editForm->handleRequest($request); + + $event = new PrivacyEvent($person, array( + 'element_class' => Activity::class, + 'element_id' => $entity->getId(), + 'action' => 'update' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); if ($editForm->isValid()) { $em->flush(); diff --git a/DependencyInjection/ChillActivityExtension.php b/DependencyInjection/ChillActivityExtension.php index ab0059155..0a5144a17 100644 --- a/DependencyInjection/ChillActivityExtension.php +++ b/DependencyInjection/ChillActivityExtension.php @@ -53,6 +53,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf $loader->load('services/repositories.yml'); $loader->load('services/fixtures.yml'); $loader->load('services/menu.yml'); + $loader->load('services/controller.yml'); } public function prepend(ContainerBuilder $container) diff --git a/Export/Aggregator/ActivityTypeAggregator.php b/Export/Aggregator/ActivityTypeAggregator.php index 544ae98ac..3dec544ba 100644 --- a/Export/Aggregator/ActivityTypeAggregator.php +++ b/Export/Aggregator/ActivityTypeAggregator.php @@ -63,32 +63,6 @@ class ActivityTypeAggregator implements AggregatorInterface // add select element $qb->addSelect(sprintf('IDENTITY(activity.type) AS %s', self::KEY)); - // make a jointure only if needed - /*$join = $qb->getDQLPart('join'); - if ( - (array_key_exists('activity', $join) - && - !$this->checkJoinAlreadyDefined($join['activity'], 'reasons') - ) - OR - (! array_key_exists('activity', $join)) - ) { - $qb->add( - 'join', - array('activity' => - new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons') - ), - true); - } - - // join category if necessary - if ($alias === 'activity_categories_id') { - // add join only if needed - if (!$this->checkJoinAlreadyDefined($qb->getDQLPart('join')['activity'], 'category')) { - $qb->join('reasons.category', 'category'); - } - }*/ - // add the "group by" part $groupBy = $qb->addGroupBy(self::KEY); } diff --git a/Export/Filter/ActivityTypeFilter.php b/Export/Filter/ActivityTypeFilter.php new file mode 100644 index 000000000..b3616da10 --- /dev/null +++ b/Export/Filter/ActivityTypeFilter.php @@ -0,0 +1,148 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\ActivityBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\Query\Expr; +use Symfony\Component\Security\Core\Role\Role; +use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\Expr\Join; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\ActivityBundle\Entity\ActivityType; + +/** + * + * + */ +class ActivityTypeFilter implements FilterInterface, + ExportElementValidatedInterface +{ + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * The repository for activity reasons + * + * @var EntityRepository + */ + protected $typeRepository; + + public function __construct( + TranslatableStringHelper $helper, + EntityRepository $typeRepository + ) { + $this->translatableStringHelper = $helper; + $this->typeRepository = $typeRepository; + } + + + public function alterQuery(QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $clause = $qb->expr()->in('activity.type', ':selected_activity_types'); + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('selected_activity_types', $data['types']); + } + + /** + * Check if a join between Activity and Reason is already defined + * + * @param Join[] $joins + * @return boolean + */ + private function checkJoinAlreadyDefined(array $joins, $alias) + { + foreach ($joins as $join) { + if ($join->getAlias() === $alias) { + return true; + } + } + + return false; + } + + public function applyOn() + { + return 'activity'; + } + + public function buildForm(FormBuilderInterface $builder) + { + //create a local copy of translatableStringHelper + $helper = $this->translatableStringHelper; + + $builder->add('types', EntityType::class, array( + 'class' => ActivityType::class, + 'choice_label' => function (ActivityType $type) use ($helper) { + return $helper->localize($type->getName()); + }, + 'multiple' => true, + 'expanded' => false + )); + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if ($data['types'] === null || count($data['types']) === 0) { + $context->buildViolation("At least one type must be choosen") + ->addViolation(); + } + } + + public function getTitle() + { + return 'Filter by activity type'; + } + + public function addRole() + { + return new Role(ActivityStatsVoter::STATS); + } + + public function describeAction($data, $format = 'string') + { + // collect all the reasons'name used in this filter in one array + $reasonsNames = array_map( + function(ActivityType $t) { + return "\"".$this->translatableStringHelper->localize($t->getName())."\""; + }, + $this->typeRepository->findBy(array('id' => $data['types']->toArray())) + ); + + return array("Filtered by activity type: only %list%", + ["%list%" => implode(", ", $reasonsNames)]); + } +} diff --git a/Resources/config/services/controller.yml b/Resources/config/services/controller.yml new file mode 100644 index 000000000..075c52679 --- /dev/null +++ b/Resources/config/services/controller.yml @@ -0,0 +1,5 @@ +services: + Chill\ActivityBundle\Controller\ActivityController: + arguments: + $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' + tags: ['controller.service_arguments'] diff --git a/Resources/config/services/export.yml b/Resources/config/services/export.yml index 8fdb98bbf..222e8b5e0 100644 --- a/Resources/config/services/export.yml +++ b/Resources/config/services/export.yml @@ -31,6 +31,14 @@ services: tags: - { name: chill.export_filter, alias: 'activity_reason_filter' } + chill.activity.export.type_filter: + class: Chill\ActivityBundle\Export\Filter\ActivityTypeFilter + arguments: + - "@chill.main.helper.translatable_string" + - "@chill_activity.repository.activity_type" + tags: + - { name: chill.export_filter, alias: 'activity_type_filter' } + chill.activity.export.date_filter: class: Chill\ActivityBundle\Export\Filter\ActivityDateFilter arguments: diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index b4bc1ad05..f5e28fd33 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -114,11 +114,12 @@ Count activities by various parameters.: Compte le nombre d'activités enregistr Sum activity duration: Total de la durée des activités Sum activities duration by various parameters.: Additionne la durée des activités en fonction de différents paramètres. List activities: Liste les activités +Number of activities: Nombre d'activités #filters Filter by reason: Filtrer par sujet d'activité 'Filtered by reasons: only %list%': 'Filtré par sujet: seulement %list%' - +'Filtered by activity type: only %list%': "Filtré par type d'activity: seulement %list%" Filtered by date activity: Filtrer par date d'activité Activities after this date: Activités après cette date Activities before this date: Activités avant cette date @@ -131,6 +132,7 @@ Implied in an activity before this date: Impliqué dans une activité avant cett Filtered by person having an activity between %date_from% and %date_to% with reasons %reasons_name%: Filtré par personnes associées à une activité entre %date_from% et %date_to% avec les sujets %reasons_name% Activity reasons for those activities: Sujets de ces activités +Filter by activity type: Filtrer par type d'activité #aggregators Aggregate by activity reason: Aggréger par sujet de l'activité @@ -138,8 +140,10 @@ By reason: Par sujet By category of reason: Par catégorie de sujet Reason's level: Niveau du sujet Aggregate by activity type: Aggréger par date d'activité -Aggregate by activity type: Aggréger par type d'activité Activity type: Type d'activité Group by reasons: Sujet d'activité Activity user: Utilisateur lié à l'activity Aggregate by activity user: Aggréger par utilisateur lié à l'activité +Aggregate by activity type: Aggréger par type d'activité +Activity type: Type d'activité +Group by reasons: Sujet d'activité