diff --git a/DependencyInjection/ChillReportExtension.php b/DependencyInjection/ChillReportExtension.php index f6f8f7551..56d8effc5 100644 --- a/DependencyInjection/ChillReportExtension.php +++ b/DependencyInjection/ChillReportExtension.php @@ -28,6 +28,7 @@ class ChillReportExtension extends Extension implements PrependExtensionInterfac $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); $loader->load('services/fixtures.yml'); + $loader->load('services/export.yml'); } /** diff --git a/Export/Export/ReportList.php b/Export/Export/ReportList.php new file mode 100644 index 000000000..3da656aa0 --- /dev/null +++ b/Export/Export/ReportList.php @@ -0,0 +1,547 @@ + + */ +class ReportList implements ListInterface, ExportElementValidatedInterface +{ + /** + * + * @var CustomFieldsGroup + */ + protected $customfieldsGroup; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + /** + * + * @var CustomFieldProvider + */ + protected $customFieldProvider; + + protected $em; + + protected $fields = array( + 'person_id', 'person_firstName', 'person_lastName', 'person_birthdate', + 'person_placeOfBirth', 'person_gender', 'person_memo', 'person_email', 'person_phonenumber', + 'person_countryOfBirth', 'person_nationality', 'person_address_street_address_1', + 'person_address_street_address_2', 'person_address_valid_from', 'person_address_postcode_label', + 'person_address_postcode_code', 'person_address_country_name', 'person_address_country_code', + 'report_id', 'report_user', 'report_date', 'report_scope' + ); + + protected $slugs = []; + + function __construct( + CustomFieldsGroup $customfieldsGroup, + TranslatableStringHelper $translatableStringHelper, + TranslatorInterface $translator, + CustomFieldProvider $customFieldProvider, + EntityManagerInterface $em + ) { + $this->customfieldsGroup = $customfieldsGroup; + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + $this->customFieldProvider = $customFieldProvider; + $this->em = $em; + } + + + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + { + $choices = array_combine($this->fields, $this->fields); + + foreach ($this->getCustomFields() as $cf) { + $choices + [$this->translatableStringHelper->localize($cf->getName())] + = + $cf->getSlug(); + } + + // Add a checkbox to select fields + $builder->add('fields', ChoiceType::class, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $choices, + 'label' => 'Fields to include in export', + 'choice_attr' => function($val, $key, $index) { + // add a 'data-display-target' for address fields + if (substr($val, 0, 8) === 'address_') { + return ['data-display-target' => 'address_date']; + } else { + return []; + } + }, + 'choice_label' => function($key, $label) { + switch (\substr($key, 0, 7)) { + case 'person_': + return $this->translator->trans(\substr($key, 7, \strlen($key) - 7)). + ' ('.$this->translator->trans('Person').')'; + case 'report_': + return $this->translator->trans(\ucfirst(\substr($key, 7, \strlen($key) - 7))). + ' ('.$this->translator->trans('Report').')'; + default: + return $label. + ' ('.$this->translator->trans("Report's question").')';; + } + }, + 'constraints' => [new Callback(array( + 'callback' => function($selected, ExecutionContextInterface $context) { + if (count($selected) === 0) { + $context->buildViolation('You must select at least one element') + ->atPath('fields') + ->addViolation(); + } + } + ))] + )); + + // add a date field for addresses + $builder->add('address_date', ChillDateType::class, array( + 'label' => "Address valid at this date", + 'data' => new \DateTime(), + 'required' => false, + 'block_name' => 'list_export_form_address_date' + )); + } + + public function validateForm($data, ExecutionContextInterface $context) + { + // get the field starting with address_ + $addressFields = array_filter(function($el) { + return substr($el, 0, 8) === 'address_'; + }, $this->fields); + + // check if there is one field starting with address in data + if (count(array_intersect($data['fields'], $addressFields)) > 0) { + // if a field address is checked, the date must not be empty + if (empty($data['address_date'])) { + $context + ->buildViolation("You must set this date if an address is checked") + ->atPath('address_date') + ->addViolation(); + } + } + } + + /** + * Get custom fields associated with person + * + * @return CustomField[] + */ + private function getCustomFields() + { + return \array_filter($this->customfieldsGroup + ->getCustomFields()->toArray(), function(CustomField $cf) { + return $cf->getType() !== 'title'; + }); + } + + public function getAllowedFormattersTypes() + { + return array(FormatterInterface::TYPE_LIST); + } + + public function getDescription() + { + return $this->translator->trans( + "Generate list of report '%type%'", + [ + '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()) + ] + ); + } + + /** + * {@inheritDoc} + * + * @param type $key + * @param array $values + * @param type $data + * @return type + */ + public function getLabels($key, array $values, $data) + { + switch ($key) { + case 'person_birthdate': + case 'report_date': + // for birthdate or report date, we have to transform the string into a date + // to format the date correctly. + return function($value) use ($key) { + if ($value === '_header') { + return $key === 'person_birthdate' ? 'birthdate' : 'report_date'; + } + + if (empty($value)) + { + return ""; + } + + if ($key === 'person_birthdate') { + $date = \DateTime::createFromFormat('Y-m-d', $value); + } else { + $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value); + } + // check that the creation could occurs. + if ($date === false) { + throw new \Exception(sprintf("The value %s could " + . "not be converted to %s", $value, \DateTime::class)); + } + + return $date->format('d-m-Y'); + }; + case 'report_scope': + $qb = $this->em->getRepository(Scope::class) + ->createQueryBuilder('s'); + $qb->addSelect('s.name') + ->addSelect('s.id') + ->where($qb->expr()->in('s.id', $values)) + ; + $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); + + foreach($rows as $row) { + $scopes[$row['id']] = $this->translatableStringHelper + ->localize($row['name']); + } + + return function($value) use ($scopes) { + if ($value === '_header') { + return 'circle'; + } + + return $scopes[$value]; + }; + case 'report_user': + $qb = $this->em->getRepository(User::class) + ->createQueryBuilder('u'); + $qb->addSelect('u.username') + ->addSelect('u.id') + ->where($qb->expr()->in('u.id', $values)) + ; + $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); + + foreach($rows as $row) { + $users[$row['id']] = $row['username']; + } + + return function($value) use ($users) { + if ($value === '_header') { + return 'user'; + } + + return $users[$value]; + }; + case 'person_gender' : + // for gender, we have to translate men/women statement + return function($value) { + if ($value === '_header') { return 'gender'; } + + return $this->translator->trans($value); + }; + case 'person_countryOfBirth': + case 'person_nationality': + $countryRepository = $this->em + ->getRepository('ChillMainBundle:Country'); + + // load all countries in a single query + $countryRepository->findBy(array('countryCode' => $values)); + + return function($value) use ($key, $countryRepository) { + if ($value === '_header') { return \strtolower($key); } + + if ($value === NULL) { + return $this->translator->trans('no data'); + } + + $country = $countryRepository->find($value); + + return $this->translatableStringHelper->localize( + $country->getName()); + }; + case 'person_address_country_name': + return function($value) use ($key) { + if ($value === '_header') { return \strtolower($key); } + + if ($value === NULL) { + return ''; + } + + return $this->translatableStringHelper->localize(json_decode($value, true)); + }; + default: + // for fields which are associated with person + if (in_array($key, $this->fields)) { + return function($value) use ($key) { + if ($value === '_header') { return \strtolower($key); } + + return $value; + + }; + } else { + return $this->getLabelForCustomField($key, $values, $data); + } + } + + } + + private function getLabelForCustomField($key, array $values, $data) + { + // for fields which are custom fields + /* @var $cf CustomField */ + $cf = $this->em + ->getRepository(CustomField::class) + ->findOneBy(array('slug' => $this->DQLToSlug($key))); + + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); + $defaultFunction = function($value) use ($cf) { + if ($value === '_header') { + return $this->translatableStringHelper->localize($cf->getName()); + } + + return $this->customFieldProvider + ->getCustomFieldByType($cf->getType()) + ->render(json_decode($value, true), $cf, 'csv'); + }; + + if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { + return function($value) use ($cf, $cfType, $key) { + $slugChoice = $this->extractInfosFromSlug($key)['additionnalInfos']['choiceSlug']; + $decoded = \json_decode($value, true); + + if ($value === '_header') { + + $label = $cfType->getChoices($cf)[$slugChoice]; + + return $this->translatableStringHelper->localize($cf->getName()) + .' | '.$label; + } + + if ($slugChoice === '_other' and $cfType->isChecked($cf, $choiceSlug, $decoded)) { + return $cfType->extractOtherValue($cf, $decoded); + } else { + return $cfType->isChecked($cf, $slugChoice, $decoded); + } + }; + + } else { + return $defaultFunction; + } + } + + public function getQueryKeys($data) + { + $fields = array(); + + foreach ($data['fields'] as $key) { + if (in_array($key, $this->fields)) { + $fields[] = $key; + } + } + + // add the key from slugs and return + return \array_merge($fields, \array_keys($this->slugs)); + } + + /** + * clean a slug to be usable by DQL + * + * @param string $slugsanitize + * @param string $type the type of the customfield, if required (currently only for choices) + * @return string + */ + private function slugToDQL($slug, $type = "default", array $additionalInfos = []) + { + $uid = 'slug_'.\uniqid(); + + $this->slugs[$uid] = [ + 'slug' => $slug, + 'type' => $type, + 'additionnalInfos' => $additionalInfos + ]; + + return $uid; + } + + private function DQLToSlug($cleanedSlug) + { + return $this->slugs[$cleanedSlug]['slug']; + } + + /** + * + * @param type $cleanedSlug + * @return an array with keys = 'slug', 'type', 'additionnalInfo' + */ + private function extractInfosFromSlug($slug) + { + return $this->slugs[$slug]; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle() + { + return $this->translator->trans( + "List for report '%type%'", + [ + '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()) + ] + ); + } + + public function getType() + { + return 'report'; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) + { + $centers = array_map(function($el) { return $el['center']; }, $acl); + + // throw an error if any fields are present + if (!\array_key_exists('fields', $data)) { + throw new \Doctrine\DBAL\Exception\InvalidArgumentException("any fields " + . "have been checked"); + } + + $qb = $this->em->createQueryBuilder(); + + foreach ($this->fields as $f) { + if (!\in_array($f, $data['fields'])) { + continue; + } + + switch ($f) { + case 'person_countryOfBirth': + case 'person_nationality': + $suffix = \substr($f, 7); + $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $suffix, $f)); + break; + case 'person_address_street_address_1': + case 'person_address_street_address_2': + case 'person_address_valid_from': + case 'person_address_postcode_label': + case 'person_address_postcode_code': + case 'person_address_country_name': + case 'person_address_country_code': + // remove 'person_' + $suffix = \substr($f, 7); + + $qb->addSelect(sprintf( + 'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s', + // get the part after address_ + strtoupper(substr($suffix, 8)), + $f)); + $qb->setParameter('address_date', $data['address_date']); + break; + case 'report_scope': + $qb->addSelect(sprintf('IDENTITY(report.scope) AS %s', 'report_scope')); + break; + case 'report_user': + $qb->addSelect(sprintf('IDENTITY(report.user) AS %s', 'report_user')); + break; + default: + $prefix = \substr($f, 0, 7); + $suffix = \substr($f, 7); + + switch($prefix) { + case 'person_': + $qb->addSelect(sprintf('person.%s as %s', $suffix, $f)); + break; + case 'report_': + $qb->addSelect(sprintf('report.%s as %s', $suffix, $f)); + break; + default: + throw new \LogicException("this prefix $prefix should " + . "not be encountered. Full field: $f"); + } + } + + } + + foreach ($this->getCustomFields() as $cf) { + + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); + + if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { + foreach($cfType->getChoices($cf) as $choiceSlug => $label) { + $slug = $this->slugToDQL($cf->getSlug(), 'choice', [ 'choiceSlug' => $choiceSlug ]); + $qb->addSelect( + sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', + $slug, $slug)); + $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); + } + } else { + $slug = $this->slugToDQL($cf->getSlug()); + $qb->addSelect( + sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', + $slug, $slug)); + $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); + } + } + + $qb + ->from(Report::class, 'report') + ->leftJoin('report.person', 'person') + ->join('person.center', 'center') + ->andWhere($qb->expr()->eq('report.cFGroup', ':cFGroup')) + ->setParameter('cFGroup', $this->customfieldsGroup) + ->andWhere('center IN (:authorized_centers)') + ->setParameter('authorized_centers', $centers); + ; + + + return $qb; + } + + public function requiredRole() + { + return new Role(ReportVoter::LISTS); + } + + public function supportsModifiers() + { + return [Declarations::PERSON_IMPLIED_IN, Declarations::PERSON_TYPE, 'report']; + } +} diff --git a/Export/Export/ReportListProvider.php b/Export/Export/ReportListProvider.php new file mode 100644 index 000000000..e7b8a2a79 --- /dev/null +++ b/Export/Export/ReportListProvider.php @@ -0,0 +1,77 @@ + + */ +class ReportListProvider implements ExportElementsProviderInterface +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * + * @var CustomFieldProvider + */ + protected $customFieldProvider; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + function __construct( + EntityManagerInterface $em, + TranslatableStringHelper $translatableStringHelper, + TranslatorInterface $translator, + CustomFieldProvider $customFieldProvider + ) { + $this->em = $em; + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + $this->customFieldProvider = $customFieldProvider; + } + + + + public function getExportElements() + { + $groups = $this->em->getRepository(CustomFieldsGroup::class) + ->findBy([ 'entity' => Report::class ]) + ; + $reports = []; + + foreach ($groups as $group) { + $reports[$group->getId()] = new ReportList( + $group, + $this->translatableStringHelper, + $this->translator, + $this->customFieldProvider, + $this->em); + } + + return $reports; + } +} diff --git a/Export/Filter/ReportDateFilter.php b/Export/Filter/ReportDateFilter.php new file mode 100644 index 000000000..379d9ccda --- /dev/null +++ b/Export/Filter/ReportDateFilter.php @@ -0,0 +1,75 @@ + + */ +class ReportDateFilter implements FilterInterface +{ + + public function addRole() + { + return null; + } + + public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $clause = $qb->expr()->between('report.date', ':report_date_filter_date_from', + ':report_date_filter_date_to'); + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('report_date_filter_date_from', $data['date_from']); + $qb->setParameter('report_date_filter_date_to', $data['date_to']); + } + + public function applyOn() + { + return 'report'; + } + + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + { + $builder->add('date_from', ChillDateType::class, array( + 'label' => "Report is after this date", + 'data' => new \DateTime(), + )); + + $builder->add('date_to', ChillDateType::class, array( + 'label' => "Report is before this date", + 'data' => new \DateTime(), + )); + } + + public function describeAction($data, $format = 'string') + { + return array('Filtered by report\'s date: ' + . 'between %date_from% and %date_to%', array( + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + )); + } + + public function getTitle() + { + return 'Filter by report\'s date'; + } +} diff --git a/Resources/config/services/export.yml b/Resources/config/services/export.yml new file mode 100644 index 000000000..4b9d7a7e7 --- /dev/null +++ b/Resources/config/services/export.yml @@ -0,0 +1,13 @@ +services: + Chill\ReportBundle\Export\Export\ReportListProvider: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + $translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + $customFieldProvider: '@Chill\CustomFieldsBundle\Service\CustomFieldProvider' + tags: + - { name: chill.export_elements_provider, prefix: 'report' } + + Chill\ReportBundle\Export\Filter\ReportDateFilter: + tags: + - { name: chill.export_filter, alias: 'report_date' } diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 04cc0c178..6ea3a2600 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -43,4 +43,12 @@ No report registered for this person.: Aucun rapport pour cette personne. #roles CHILL_REPORT_UPDATE: Modifier les rapports CHILL_REPORT_SEE: Voir les rapports -CHILL_REPORT_CREATE: Créer des rapports \ No newline at end of file +CHILL_REPORT_CREATE: Créer des rapports + +#exports +"List for report '%type%'": Liste des rapports "%type%" +"Generate list of report '%type%'": Génère une liste des rapports "%type%" +"Report's question": Question du rapport +Filter by report's date: Filtrer par date de rapport +Report is after this date: Rapports après cette date +Report is before this date: Rapports avant cette date \ No newline at end of file diff --git a/Security/Authorization/ReportVoter.php b/Security/Authorization/ReportVoter.php index 2950abd8d..5741b567b 100644 --- a/Security/Authorization/ReportVoter.php +++ b/Security/Authorization/ReportVoter.php @@ -26,6 +26,7 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\ReportBundle\Entity\Report; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Center; /** @@ -38,6 +39,7 @@ class ReportVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte const CREATE = 'CHILL_REPORT_CREATE'; const SEE = 'CHILL_REPORT_SEE'; const UPDATE = 'CHILL_REPORT_UPDATE'; + const LISTS = 'CHILL_REPORT_LISTS'; /** * @@ -58,8 +60,8 @@ class ReportVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte return \in_array($attribute, [ self::CREATE, self::UPDATE, self::SEE ]); - } else { - return false; + } elseif ($subject instanceof Center) { + return $attribute === self::LISTS; } } @@ -75,12 +77,12 @@ class ReportVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte public function getRoles() { - return [self::CREATE, self::UPDATE, self::SEE]; + return [self::CREATE, self::UPDATE, self::SEE, self::LISTS]; } public function getRolesWithoutScope() { - return array(); + return array(self::LISTS); } public function getRolesWithHierarchy()