diff --git a/.gitignore b/.gitignore index 4b827b7fb..c71baeafc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Tests/Fixtures/App/app/DoctrineMigrations/* Test/Fixtures/App/app/DoctrineMigrations/* Test/Fixtures/App/app/cache/* Test/Fixtures/App/app/config/parameters.yml +/nbproject/private/ \ No newline at end of file diff --git a/Export/Aggregator/ReasonAggregator.php b/Export/Aggregator/ReasonAggregator.php index 5698d4071..c6504ae7b 100644 --- a/Export/Aggregator/ReasonAggregator.php +++ b/Export/Aggregator/ReasonAggregator.php @@ -24,6 +24,8 @@ use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Export\AggregatorInterface; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; +use Doctrine\ORM\EntityRepository; +use Chill\MainBundle\Templating\TranslatableStringHelper; /** * @@ -32,26 +34,65 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter; */ class ReasonAggregator implements AggregatorInterface { + /** + * + * @var EntityRepository + */ + protected $categoryRepository; + + /** + * + * @var EntityRepository + */ + protected $reasonRepository; + + /** + * + * @var TranslatableStringHelper + */ + protected $stringHelper; + + public function __construct( + EntityRepository $categoryRepository, + EntityRepository $reasonRepository, + TranslatableStringHelper $stringHelper + ) { + $this->categoryRepository = $categoryRepository; + $this->reasonRepository = $reasonRepository; + $this->stringHelper = $stringHelper; + } + public function alterQuery(QueryBuilder $qb, $data) { // add select element - if ($data['level'] === 'reason') { - $elem = 'reason.id'; - $alias = 'activity_reason_id'; - } elseif ($data['level'] === 'category') { + if ($data['level'] === 'reasons') { + $elem = 'reasons.id'; + $alias = 'activity_reasons_id'; + } elseif ($data['level'] === 'categories') { $elem = 'category.id'; - $alias = 'activity_category_id'; + $alias = 'activity_categories_id'; } else { - throw new \RuntimeException('the data provided are not recognised'); + throw new \RuntimeException('the data provided are not recognized'); } $qb->addSelect($elem.' as '.$alias); + // make a jointure only if needed + // add a join to reasons only if needed + if (array_key_exists('activity', $qb->getDQLPart('join'))) { + // we want to avoid multiple join on same object + if (!$this->checkJoinAlreadyDefined($qb->getDQLPart('join')['activity'], 'reasons')) { + $qb->add('join', new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons')); + } + } else { + $qb->join('activity.reasons', 'reasons'); + } - // make a jointure - $qb->join('activity.reason', 'reason'); // join category if necessary - if ($alias === 'activity_category_id') { - $qb->join('reason.category', 'category'); + 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 @@ -63,6 +104,24 @@ class ReasonAggregator implements AggregatorInterface $qb->groupBy($alias); } } + + /** + * Check if a join between Activity and another alias + * + * @param Join[] $joins + * @param string $alias the alias to search for + * @return boolean + */ + private function checkJoinAlreadyDefined(array $joins, $alias) + { + foreach ($joins as $join) { + if ($join->getAlias() === $alias) { + return true; + } + } + + return false; + } public function applyOn() { @@ -73,12 +132,13 @@ class ReasonAggregator implements AggregatorInterface { $builder->add('level', 'choice', array( 'choices' => array( - 'By reason' => 'reason', - 'By category of reason' => 'category' + 'By reason' => 'reasons', + 'By category of reason' => 'categories' ), 'choices_as_values' => true, 'multiple' => false, - 'expanded' => true + 'expanded' => true, + 'label' => 'Reason\'s level' )); } @@ -94,17 +154,54 @@ class ReasonAggregator implements AggregatorInterface public function getLabels($key, array $values, $data) { - return array_combine($values, $values); + // for performance reason, we load data from db only once + switch ($data['level']) { + case 'reasons': + $this->reasonRepository->findBy(array('id' => $values)); + break; + case 'categories': + $this->categoryRepository->findBy(array('id' => $values)); + break; + default: + throw new \RuntimeException(sprintf("the level data '%s' is invalid", + $data['level'])); + } + + return function($value) use ($data) { + if ($value === '_header') { + return $data['level'] === 'reasons' ? + 'Group by reasons' + : + 'Group by categories of reason' + ; + } + + switch ($data['level']) { + case 'reasons': + $n = $this->reasonRepository->find($value) + ->getName() + ; + break; + case 'categories': + $n = $this->categoryRepository->find($value) + ->getName() + ; + break; + // no need for a default : the default was already set above + } + + return $this->stringHelper->localize($n); + }; } public function getQueryKeys($data) { // add select element - if ($data['level'] === 'reason') { - return array('activity_reason_id'); - } elseif ($data['level'] === 'category') { - return array ('activity_category_id'); + if ($data['level'] === 'reasons') { + return array('activity_reasons_id'); + } elseif ($data['level'] === 'categories') { + return array ('activity_categories_id'); } else { throw new \RuntimeException('the data provided are not recognised'); } diff --git a/Export/Export/CountActivity.php b/Export/Export/CountActivity.php index 57ce19369..cdab3dd74 100644 --- a/Export/Export/CountActivity.php +++ b/Export/Export/CountActivity.php @@ -102,10 +102,13 @@ class CountActivity implements ExportInterface throw new \LogicException("the key $key is not used by this export"); } - $labels = array_combine($values, $values); - $labels['_header'] = 'Number of activities'; - - return $labels; + return function($value) { + return $value === '_header' ? + 'Number of activities' + : + $value + ; + }; } public function getQueryKeys($data) diff --git a/Export/Filter/ActivityReasonFilter.php b/Export/Filter/ActivityReasonFilter.php index 9f2059fbe..32b05b01e 100644 --- a/Export/Filter/ActivityReasonFilter.php +++ b/Export/Filter/ActivityReasonFilter.php @@ -28,6 +28,8 @@ use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\Expr\Join; /** * @@ -40,18 +42,38 @@ class ActivityReasonFilter implements FilterInterface * * @var TranslatableStringHelper */ - public $translatableStringHelper; + protected $translatableStringHelper; - public function __construct(TranslatableStringHelper $helper) - { + /** + * The repository for activity reasons + * + * @var EntityRepository + */ + protected $reasonRepository; + + public function __construct( + TranslatableStringHelper $helper, + EntityRepository $reasonRepository + ) { $this->translatableStringHelper = $helper; + $this->reasonRepository = $reasonRepository; } public function alterQuery(QueryBuilder $qb, $data) { $where = $qb->getDQLPart('where'); - $clause = $qb->expr()->in('activity.reason', ':selected_activity_reasons'); + $join = $qb->getDQLPart('join'); + $clause = $qb->expr()->in('reasons', ':selected_activity_reasons'); + // add a join to reasons only if needed + if (array_key_exists('activity', $join)) { + // we want to avoid multiple join on same object + if (!$this->checkJoinAlreadyDefined($join['activity'])) { + $qb->add('join', new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons')); + } + } else { + $qb->join('activity.reasons', 'reasons'); + } if ($where instanceof Expr\Andx) { $where->add($clause); @@ -62,6 +84,23 @@ class ActivityReasonFilter implements FilterInterface $qb->add('where', $where); $qb->setParameter('selected_activity_reasons', $data['reasons']); } + + /** + * Check if a join between Activity and Reason is already defined + * + * @param Join[] $joins + * @return boolean + */ + private function checkJoinAlreadyDefined(array $joins) + { + foreach ($joins as $join) { + if ($join->getAlias() === 'reasons') { + return true; + } + } + + return false; + } public function applyOn() { @@ -95,4 +134,18 @@ class ActivityReasonFilter implements FilterInterface { return new Role(ActivityVoter::SEE); } + + public function describeAction($data, $format = 'string') + { + // collect all the reasons'name used in this filter in one array + $reasonsNames = array_map( + function(ActivityReason $r) { + return "\"".$this->translatableStringHelper->localize($r->getName())."\""; + }, + $this->reasonRepository->findBy(array('id' => $data['reasons']->toArray())) + ); + + return array("Filtered by reasons: only %list%", + ["%list%" => implode(", ", $reasonsNames)]); + } } diff --git a/Resources/config/services/export.yml b/Resources/config/services/export.yml index 94875c3c2..26b69ebab 100644 --- a/Resources/config/services/export.yml +++ b/Resources/config/services/export.yml @@ -10,10 +10,15 @@ services: class: Chill\ActivityBundle\Export\Filter\ActivityReasonFilter arguments: - "@chill.main.helper.translatable_string" + - "@chill_activity.repository.reason" tags: - { name: chill.export_filter, alias: 'activity_reason_filter' } chill.activity.export.reason_aggregator: class: Chill\ActivityBundle\Export\Aggregator\ReasonAggregator + arguments: + - "@chill_activity.repository.reason_category" + - "@chill_activity.repository.reason" + - "@chill.main.helper.translatable_string" tags: - { name: chill.export_aggregator, alias: activity_reason } diff --git a/Resources/config/services/repositories.yml b/Resources/config/services/repositories.yml index 967c67bc3..2867782b4 100644 --- a/Resources/config/services/repositories.yml +++ b/Resources/config/services/repositories.yml @@ -4,3 +4,15 @@ services: factory: ['@doctrine.orm.entity_manager', getRepository] arguments: - 'Chill\ActivityBundle\Entity\ActivityType' + + chill_activity.repository.reason: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\ActivityBundle\Entity\ActivityReason' + + chill_activity.repository.reason_category: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\ActivityBundle\Entity\ActivityReasonCategory' diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index cfa46f634..dfa72bb7a 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -98,3 +98,13 @@ The activity has been successfully removed.: L'activité a été supprimée. # exports Count activities: Nombre d'activités Count activities by various parameters.: Compte le nombre d'activités enregistrées en fonction de différents paramètres. + +#filters +Filter by reason: Filtrer par sujet d'activité +'Filtered by reasons: only %list%': 'Filtré par sujet: seulement %list%' + +#aggregators +Aggregate by activity reason: Aggréger par sujet de l'activité +By reason: Par sujet +By category of reason: Par catégorie de sujet +Reason's level: Niveau du sujet diff --git a/Tests/Export/Aggregator/ReasonAggregatorTest.php b/Tests/Export/Aggregator/ReasonAggregatorTest.php new file mode 100644 index 000000000..f508fcde2 --- /dev/null +++ b/Tests/Export/Aggregator/ReasonAggregatorTest.php @@ -0,0 +1,93 @@ + + * + * 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\Tests\Aggregator; + +use Chill\MainBundle\Test\Export\AbstractAggregatorTest; + +/** + * + * + * @author Julien Fastré + */ +class ReasonAggregatorTest extends AbstractAggregatorTest +{ + /** + * + * @var \Chill\ActivityBundle\Export\Aggregator\ReasonAggregator + */ + private $aggregator; + + public function setUp() + { + static::bootKernel(); + + $container = static::$kernel->getContainer(); + + $this->aggregator = $container->get('chill.activity.export.reason_aggregator'); + + // add a fake request with a default locale (used in translatable string) + $prophet = new \Prophecy\Prophet; + $request = $prophet->prophesize(); + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $container->get('request_stack') + ->push($request->reveal()); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData() + { + return array( + array('level' => 'reasons'), + array('level' => 'categories') + ); + } + + public function getQueryBuilders() + { + if (static::$kernel === null) { + static::bootKernel(); + } + + $em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + return array( + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity'), + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity') + ->join('activity.reasons', 'reasons'), + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity') + ->join('activity.reasons', 'reasons') + ->join('reasons.category', 'category') + ); + } + +} diff --git a/Tests/Export/Export/CountActivityTest.php b/Tests/Export/Export/CountActivityTest.php new file mode 100644 index 000000000..4f9918fd0 --- /dev/null +++ b/Tests/Export/Export/CountActivityTest.php @@ -0,0 +1,50 @@ + + */ +class CountActivityTest extends AbstractExportTest +{ + /** + * + * @var + */ + private $export; + + public function setUp() + { + static::bootKernel(); + + /* @var $container \Symfony\Component\DependencyInjection\ContainerInterface */ + $container = self::$kernel->getContainer(); + + $this->export = $container->get('chill.activity.export.count_activity'); + } + + public function getExport() + { + return $this->export; + } + + public function getFormData() + { + return array( + array() + ); + } + + public function getModifiersCombination() + { + return array( + array('activity'), + array('activity', 'person') + ); + } + +} diff --git a/Tests/Export/Filter/ActivityReasonFilterTest.php b/Tests/Export/Filter/ActivityReasonFilterTest.php new file mode 100644 index 000000000..1c4ca4c21 --- /dev/null +++ b/Tests/Export/Filter/ActivityReasonFilterTest.php @@ -0,0 +1,109 @@ + + * + * 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\Tests\Filter; + +use Chill\MainBundle\Test\Export\AbstractFilterTest; +use Doctrine\Common\Collections\ArrayCollection; + +/** + * + * + * @author Julien Fastré + */ +class ActivityReasonFilterTest extends AbstractFilterTest +{ + /** + * + * @var \Chill\PersonBundle\Export\Filter\GenderFilter + */ + private $filter; + + public function setUp() + { + static::bootKernel(); + + $container = static::$kernel->getContainer(); + + $this->filter = $container->get('chill.activity.export.reason_filter'); + + // add a fake request with a default locale (used in translatable string) + $prophet = new \Prophecy\Prophet; + $request = $prophet->prophesize(); + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $container->get('request_stack') + ->push($request->reveal()); + } + + + public function getFilter() + { + return $this->filter; + } + + public function getFormData() + { + if (static::$kernel === null) { + static::bootKernel(); + } + + $em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ; + + $reasons = $em->createQuery("SELECT reason " + . "FROM ChillActivityBundle:ActivityReason reason") + ->getResult(); + + // generate an array of 5 different combination of results + for ($i=0; $i < 5; $i++) { + $r[] = array('reasons' => new ArrayCollection(array_splice($reasons, ($i + 1) * -1))); + } + + return $r; + } + + public function getQueryBuilders() + { + if (static::$kernel === null) { + static::bootKernel(); + } + + $em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + return array( + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity'), + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity') + ->join('activity.reasons', 'reasons'), + $em->createQueryBuilder() + ->select('count(activity.id)') + ->from('ChillActivityBundle:Activity', 'activity') + ->join('activity.reasons', 'reasons') + ->join('reasons.category', 'category') + ); + } + +}