diff --git a/src/Bundle/ChillReport/.gitignore b/src/Bundle/ChillReport/.gitignore new file mode 100644 index 000000000..edf97d24e --- /dev/null +++ b/src/Bundle/ChillReport/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +composer.lock +Tests/Fixtures/App/app/config/parameters.yml +*~ +auth.json +/nbproject/private/ \ No newline at end of file diff --git a/src/Bundle/ChillReport/.gitlab-ci.yml b/src/Bundle/ChillReport/.gitlab-ci.yml new file mode 100644 index 000000000..126ed8194 --- /dev/null +++ b/src/Bundle/ChillReport/.gitlab-ci.yml @@ -0,0 +1,54 @@ +.test_definition: &test_definition + services: + - chill/database:latest + + before_script: + - composer config github-oauth.github.com $GITHUB_TOKEN + - php -d memory_limit=-1 /usr/local/bin/composer install --no-interaction + - cp Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml Resources/test/Fixtures/App/app/config/parameters.yml + - php Resources/test/Fixtures/App/app/console --env=test cache:warmup + - php Resources/test/Fixtures/App/app/console doctrine:migrations:migrate --env=test --no-interaction + - php Resources/test/Fixtures/App/app/console doctrine:fixtures:load --env=test --no-interaction + +stages: + - test + - build-doc + - deploy-doc + +test:php-7.2: + stage: test + image: chill/ci-image:php-7.2 + <<: *test_definition + script: php vendor/bin/phpunit + +# deploy documentation +api-doc-build: + stage: build-doc + environment: api-doc + image: chill/ci-image:php-7.2 + before_script: + - mkdir api-doc + script: apigen generate --destination api-doc/$CI_BUILD_REF_NAME/$CI_PROJECT_NAME + artifacts: + paths: + - "api-doc/" + name: api + expire_in: '2h' + only: + - master + - tags + +api-doc-deploy: + stage: deploy-doc + image: pallet/swiftclient:latest + before_script: + # test that CONTAINER_API variable is set + - if [ -z ${CONTAINER_API+x} ]; then echo "Please set CONTAINER_API variable"; exit -1; fi + # go to api-doc to have and url with PROJECT/BUILD + - cd api-doc + # upload, and keep files during 1 year + script: "swift upload --header \"X-Delete-After: 31536000\" $CONTAINER_API $CI_BUILD_REF_NAME/$CI_PROJECT_NAME" + only: + - master + - tags + diff --git a/src/Bundle/ChillReport/CHANGELOG.md b/src/Bundle/ChillReport/CHANGELOG.md new file mode 100644 index 000000000..e0ed03792 --- /dev/null +++ b/src/Bundle/ChillReport/CHANGELOG.md @@ -0,0 +1,18 @@ + +Version 1.5.1 +============= + +- [list export] fix error "all custom fields are shown" + +Version 1.5.2 +============= + +- add privacy events to report list / view +- add privacy events to report edit / update + +Master branch +============= + + + + diff --git a/src/Bundle/ChillReport/ChillReportBundle.php b/src/Bundle/ChillReport/ChillReportBundle.php new file mode 100644 index 000000000..967796d8b --- /dev/null +++ b/src/Bundle/ChillReport/ChillReportBundle.php @@ -0,0 +1,9 @@ + + * + * 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\ReportBundle\Controller; + +use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\PersonBundle\Privacy\PrivacyEvent; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Chill\PersonBundle\Entity\Person; +use Chill\ReportBundle\Entity\Report; +use Chill\ReportBundle\Form\ReportType; + + +/** + * Class ReportController + * + * @package Chill\ReportBundle\Controller + */ +class ReportController extends AbstractController +{ + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + /** + * @var PaginatorFactory + */ + protected $paginator; + + /** + * ReportController constructor. + * + * @param EventDispatcherInterface $eventDispatcher + * @param AuthorizationHelper $authorizationHelper + * @param PaginatorFactory $paginator + */ + public function __construct( + EventDispatcherInterface $eventDispatcher, + AuthorizationHelper $authorizationHelper, + PaginatorFactory $paginator + ) + { + $this->eventDispatcher = $eventDispatcher; + $this->authorizationHelper = $authorizationHelper; + $this->paginator = $paginator; + } + + + /** + * List all the report entities for a given person. + * + * @param integer $person_id The id of the person. + * @param Request $request The request + * @return Response The web page. + */ + public function listAction($person_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $person = $em->getRepository('ChillPersonBundle:Person')->find($person_id); + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + + $reachableScopes = $this->authorizationHelper + ->getReachableScopes($this->getUser(), new Role('CHILL_REPORT_SEE'), + $person->getCenter()); + + $total = $em + ->createQuery("SELECT COUNT(r.id) FROM ChillReportBundle:Report r " + . "WHERE r.person = :person AND r.scope IN (:scopes) ") + ->setParameter('person', $person) + ->setParameter('scopes', $reachableScopes) + ->getSingleScalarResult(); + + // get the PaginatorFactory + $paginator = $this->paginator->create($total); + + $reports = $em->createQuery('SELECT r + FROM ChillReportBundle:Report r + WHERE r.person = :person AND r.scope IN (:scopes) + ORDER BY r.date DESC') + ->setParameter('person', $person) + ->setParameter('scopes', $reachableScopes) + ->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber()) + ->setMaxResults($paginator->getItemsPerPage()) + ->getResult() + ; + + $event = new PrivacyEvent($person, array( + 'element_class' => Report::class, + 'action' => 'list' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillReportBundle:Report:list.html.twig', array( + 'reports' => $reports, + 'person' => $person, + 'paginator' => $paginator + )); + } + + /** + * Display a form for selecting which type of report to add for a given person + * + * @param integer $person_id The id of the person. + * @param Request $request The request + * @return Response The web page. + */ + public function selectReportTypeAction($person_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $person = $em->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException('Person not found!'); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person, 'access denied for person view'); + // check access on report creation for a dummy report + $this->denyAccessUnlessGranted('CHILL_REPORT_CREATE', + (new Report())->setPerson($person), 'access denied for report creation'); + + + $cFGroupId = $request->query->get('cFGroup'); + + if($cFGroupId) { + return $this->redirect( + $this->generateUrl('report_new', + array('person_id' => $person_id, 'cf_group_id' => $cFGroupId))); + } + + $cFGroups = $em->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->findByEntity('Chill\ReportBundle\Entity\Report'); + + if(count($cFGroups) === 1 ){ + return $this->redirect( + $this->generateUrl('report_new', + array('person_id' => $person_id, 'cf_group_id' => $cFGroups[0]->getId()))); + } + + $cFGroupsChoice = array(); + + foreach ($cFGroups as $cFGroup) { + $cFGroupsChoice[$cFGroup->getId()] = $cFGroup->getName($request->getLocale()); + } + + $form = $this->get('form.factory') + ->createNamedBuilder(null, FormType::class, null, array( + 'method' => 'GET', + 'csrf_protection' => false + )) + ->add('cFGroup', ChoiceType::class, array( + 'choices' => array_combine(array_values($cFGroupsChoice),array_keys($cFGroupsChoice)), + )) + ->getForm(); + + $person = $em->getRepository('ChillPersonBundle:Person')->find($person_id); + + return $this->render('ChillReportBundle:Report:select_report_type.html.twig', array( + 'form' => $form->createView(), + 'person' => $person + )); + } + + /** + * Display a form for selecting which type of report to export + * (a csv file with all the report of this type) + * + * @param Request $request The request + * @return Response The web page. + */ + public function selectReportTypeForExportAction(Request $request) + { + $cFGroupId = $request->query->get('cFGroup'); + + if($cFGroupId) { + return $this->redirect( + $this->generateUrl('report_export_list', + array('cf_group_id' => $cFGroupId))); + } + + $em = $this->getDoctrine()->getManager(); + + $cFGroups = $em->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->findByEntity('Chill\ReportBundle\Entity\Report'); + + if(count($cFGroups) === 1 ){ + return $this->redirect( + $this->generateUrl('report_export_list', + array('cf_group_id' => $cFGroups[0]->getId()))); + } + + $cFGroupsChoice = array(); + + foreach ($cFGroups as $cFGroup) { + $cFGroupsChoice[$cFGroup->getId()] = $cFGroup->getName($request->getLocale()); + } + + $form = $this->get('form.factory') + ->createNamedBuilder(null, FormType::class, null, array( + 'method' => 'GET', + 'csrf_protection' => false + )) + ->add('cFGroup', ChoiceType::class, array( + 'choices' => array_combine(array_values($cFGroupsChoice),array_keys($cFGroupsChoice)), + )) + ->getForm(); + + return $this->render('ChillReportBundle:Report:select_report_type_for_export.html.twig', array( + 'form' => $form->createView(), + 'layout_name' => "@ChillMain/Export/layout.html.twig" + )); + } + + /** + * Return a csv file with all the reports of a given type + * + * @param integer $cf_group_id The id of the report type to export + * @param Request $request The request + * @return A csv file with all the reports of the selected type + */ + public function exportAction($cf_group_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $cFGroup = $em->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup')->find($cf_group_id); + $reports = $em->getRepository('ChillReportBundle:Report')->findByCFGroup($cFGroup); + + + $response = $this->render('ChillReportBundle:Report:export.csv.twig', array( + 'reports' => $reports, + 'cf_group' => $cFGroup + )); + + $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); + $response->headers->set('Content-Disposition', 'attachment; filename="export.csv"'); + return $response; + } + + /** + * Display a form for creating a new report for a given person and of a given type + * + * @param integer $person_id The id of the person. + * @param integer $cf_group_id The id of the report type. + * @param Request $request The request + * @return Response The web page. + */ + public function newAction($person_id, $cf_group_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $person = $em->getRepository('ChillPersonBundle:Person')->find($person_id); + $cFGroup = $em + ->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->find($cf_group_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person not found"); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + // check access on report creation for a dummy report + $this->denyAccessUnlessGranted('CHILL_REPORT_CREATE', + (new Report())->setPerson($person), 'access denied for report creation'); + + if ($cFGroup === NULL){ + throw $this->createNotFoundException("custom fields group not found"); + } + + $entity = new Report(); + $entity->setUser($this->get('security.token_storage')->getToken()->getUser()); + $entity->setDate(new \DateTime('now')); + + $entity->setCFGroup($cFGroup); + + $form = $this->createCreateForm($entity, $person, $cFGroup); + + return $this->render('ChillReportBundle:Report:new.html.twig', array( + 'entity' => $entity, + 'form' => $form->createView(), + 'person' => $person + )); + } + + /** + * Create a new report for a given person and of a given type + * + * @param integer $person_id The id of the person. + * @param integer $cf_group_id The id of the report type. + * @param Request $request The request containing the form data (from the newAction) + * @return Response The web page. + */ + public function createAction($person_id, $cf_group_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $entity = new Report(); + $cFGroup = $em->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->find($cf_group_id); + + $person = $em->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if($person === NULL || $cFGroup === NULL) { + throw $this->createNotFoundException(); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + + $form = $this->createCreateForm($entity, $person, $cFGroup); + $form->handleRequest($request); + + if ($form->isValid()) { + $entity->setCFGroup($cFGroup); + $entity->setPerson($person); + + $this->denyAccessUnlessGranted('CHILL_REPORT_CREATE', $entity); + + $em->persist($entity); + $em->flush(); + + $this->get('session') + ->getFlashBag() + ->add('success', + $this->get('translator') + ->trans('Success : report created!') + ); + + return $this->redirect($this->generateUrl('report_view', + array('person_id' => $person_id,'report_id' => $entity->getId()))); + } + + + $this->get('session') + ->getFlashBag()->add('error', + $this->get('translator') + ->trans('The form is not valid. The report has not been created !') + ); + + return $this->render('ChillReportBundle:Report:new.html.twig', array( + 'entity' => $entity, + 'form' => $form->createView(), + 'person' => $person + )); + } + + /** + * Creates a form to create a Report entity. + * + * @param Report $entity The entity + * @param integer $person_id The id of the person. + * @param integer $cf_group_id The id of the report type. + * + * @return \Symfony\Component\Form\Form The form + */ + private function createCreateForm(Report $entity, Person $person, $cFGroup) + { + $form = $this->createForm(ReportType::class, $entity, array( + 'action' => $this->generateUrl('report_create', + array('person_id' => $person->getId(), + 'cf_group_id' => $cFGroup->getId())), + 'method' => 'POST', + 'cFGroup' => $cFGroup, + 'role' => new Role('CHILL_REPORT_CREATE'), + 'center' => $person->getCenter() + )); + + return $form; + } + + /** + * Find and display a report. + * + * @param integer $report_id The id of the report. + * @param integer $person_id The id of the person. + * @return Response The web page. + */ + public function viewAction($report_id, $person_id) + { + $em = $this->getDoctrine()->getManager(); + + $person = $em->getRepository('ChillPersonBundle:Person')->find($person_id); + + $entity = $em->getRepository('ChillReportBundle:Report')->find($report_id); + + if (!$entity || !$person) { + throw $this->createNotFoundException( + $this->get('translator')->trans('Unable to find this report.')); + } + + $this->denyAccessUnlessGranted('CHILL_REPORT_SEE', $entity); + + + $event = new PrivacyEvent($person, array( + 'element_class' => Report::class, + 'element_id' => $entity->getId(), + 'action' => 'view' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillReportBundle:Report:view.html.twig', array( + 'entity' => $entity, + 'person' => $person, + )); + } + + /** + * Display a form to edit an existing Report entity. + * + * @param integer $person_id The id of the person. + * @param integer $report_id The id of the report. + * @return Response The web page. + */ + public function editAction($person_id, $report_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $report = $em->getRepository('ChillReportBundle:Report')->find($report_id); + + if (!$report) { + throw $this->createNotFoundException( + $this->get('translator')->trans('Unable to find this report.')); + } + + if(intval($person_id) !== intval($report->getPerson()->getId())) { + throw new \RuntimeException( + $this->get('translator')->trans('This is not the report of the person.'), 1); + } + + $this->denyAccessUnlessGranted('CHILL_REPORT_UPDATE', $report); + + $person = $report->getPerson(); + + $editForm = $this->createEditForm($report); + + $event = new PrivacyEvent($person, array( + 'element_class' => Report::class, + 'element_id' => $report->getId(), + 'action' => 'edit' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillReportBundle:Report:edit.html.twig', array( + 'edit_form' => $editForm->createView(), + 'person' => $person, + )); + } + + /** + * Creates a form to edit a Report entity. + * + * @param Report $entity The report to edit. + * @param integer $person_id The id of the person. + * @return \Symfony\Component\Form\Form The form + */ + private function createEditForm(Report $entity) + { + $form = $this->createForm(ReportType::class, $entity, array( + 'action' => $this->generateUrl('report_update', + array('person_id' => $entity->getPerson()->getId(), + 'report_id' => $entity->getId())), + 'method' => 'PUT', + 'cFGroup' => $entity->getCFGroup(), + 'role' => new Role('CHILL_REPORT_UPDATE'), + 'center' => $entity->getPerson()->getCenter() + )); + + return $form; + } + + /** + * Web page for editing an existing report. + * + * @param integer $person_id The id of the person. + * @param integer $report_id The id of the report. + * @return Response The web page. + */ + public function updateAction($person_id, $report_id, Request $request) + { + $em = $this->getDoctrine()->getManager(); + + $report = $em->getRepository('ChillReportBundle:Report')->find($report_id); + + if (!$report) { + throw $this->createNotFoundException( + $this->get('translator')->trans('Unable to find this report.')); + } + + $this->denyAccessUnlessGranted('CHILL_REPORT_UPDATE', $report); + + $editForm = $this->createEditForm($report); + $editForm->handleRequest($request); + + if ($editForm->isValid()) { + $em->flush(); + + $this->get('session') + ->getFlashBag() + ->add('success', + $this->get('translator') + ->trans('Success : report updated!') + ); + + $person = $report->getPerson(); + + $event = new PrivacyEvent($person, array( + 'element_class' => Report::class, + 'element_id' => $report->getId(), + 'action' => 'update' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->redirect($this->generateUrl('report_view', + array('person_id' => $report->getPerson()->getId(), 'report_id' => $report_id))); + } + + $this->get('session') + ->getFlashBag() + ->add('error', + $this->get('translator') + ->trans('The form is not valid. The report has not been updated !') + ); + + return $this->render('ChillReportBundle:Report:edit.html.twig', array( + 'edit_form' => $editForm->createView(), + 'person' => $report->getPerson() + )); + } +} diff --git a/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomField.php b/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomField.php new file mode 100644 index 000000000..005f7d9ec --- /dev/null +++ b/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomField.php @@ -0,0 +1,222 @@ + + * + * 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\ReportBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\CustomFieldsBundle\Entity\CustomField; + +/** + * Load CustomField for Report into database + */ +class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 15001; + } + + public function load(ObjectManager $manager) + { + echo "loading CustomField...\n"; + + $cFTypes = [ + array('type' => 'text', 'options' => array('maxLength' => '255')), + array('type' => 'text', 'options' => array('maxLength' => '1000')), + array('type' => 'text', 'options' => array('maxLength' => '2000')), + array('type' => 'title', 'options' => array('type' => 'title')), + array('type' => 'title', 'options' => array('type' => 'subtitle')), + array('type' => 'choice', 'options' => array( + 'multiple' => false, + 'expanded'=> false, + 'other' => false, + 'choices'=> [ + array( + 'name' => array( + 'fr' => 'Options 1 FR', + 'nl' => 'Options 1 NL', + 'en' => 'Options 1 EN'), + 'active' => true, + 'slug' => 'el-1-fr'), + array( + 'name' => array( + 'fr' => 'Options 2 FR', + 'nl' => 'Options 2 NL', + 'en' => 'Options 2 EN'), + 'active' => true, + 'slug' => 'el-2-fr'), + array( + 'name' => array( + 'fr' => 'Options 2 FR', + 'nl' => 'Options 2 NL', + 'en' => 'Options 2 EN'), + 'active' => true, + 'slug' => 'el-3-fr') + ] + ) + ) + ]; + + for($i=0; $i <= 25; $i++) { + echo "CustomField {$i}\n"; + $cFType = $cFTypes[rand(0,sizeof($cFTypes) - 1)]; + + $customField = (new CustomField()) + ->setSlug("cf_report_{$i}") + ->setType($cFType['type']) + ->setOptions($cFType['options']) + ->setName(array("fr" => "CustomField {$i}")) + ->setOrdering(rand(0,1000) / 1000) + ->setCustomFieldsGroup($this->getReference('cf_group_report_'.(rand(0,3)))) + ; + + $manager->persist($customField); + } + + $this->createExpectedFields($manager); + + $manager->flush(); + } + + private function createExpectedFields(ObjectManager $manager) + { + //report logement + $reportLogement = $this->getReference('cf_group_report_logement'); + + $houseTitle = (new CustomField()) + ->setSlug('house_title') + ->setType('title') + ->setOptions(array('type' => 'title')) + ->setName(array('fr' => 'Situation de logement')) + ->setOrdering(10) + ->setCustomFieldsGroup($reportLogement) + ; + $manager->persist($houseTitle); + + $hasLogement = (new CustomField()) + ->setSlug('has_logement') + ->setName(array('fr' => 'Logement actuel')) + ->setType('choice') + ->setOptions(array( + 'multiple' => FALSE, + 'expanded' => TRUE, + 'other' => TRUE, + 'choices' => [ + array( + 'name' => ['fr' => 'Locataire d\' un logement'], + 'slug' => 'rent_house', + 'active' => true + ), + array( + 'name' => ['fr' => 'Propriétaire d\' un logement'], + 'slug' => 'own_house', + 'active' => true + ), + array( + 'name' => ['fr' => 'Par-ci, par là (amis, famille, ...)'], + 'slug' => 'here-and-there', + 'active' => true + ), + array( + 'name' => ['fr' => 'A la rue'], + 'slug' => 'street', + 'active' => true + ) + ] + + )) + ->setOrdering(20) + ->setCustomFieldsGroup($reportLogement) + ; + $manager->persist($hasLogement); + + $descriptionLogement = (new CustomField()) + ->setSlug('house-desc') + ->setName(array('fr' => 'Plaintes éventuelles sur le logement')) + ->setType('text') + ->setOptions(['maxLength' => 1500]) + ->setOrdering(30) + ->setCustomFieldsGroup($reportLogement) + ; + $manager->persist($descriptionLogement); + + + //report problems + $reportEducation = $this->getReference('cf_group_report_education'); + + $title = (new CustomField()) + ->setSlug('title') + ->setType('title') + ->setOptions(array('type' => 'title')) + ->setName(array('fr' => 'Éducation')) + ->setOrdering(10) + ->setCustomFieldsGroup($reportEducation) + ; + $manager->persist($title); + + $educationLevel = (new CustomField()) + ->setSlug('level') + ->setName(array('fr' => 'Niveau du plus haut diplôme')) + ->setType('choice') + ->setOptions(array( + 'multiple' => FALSE, + 'expanded' => FALSE, + 'other' => FALSE, + 'choices' => [ + array( + 'name' => ['fr' => 'Supérieur'], + 'slug' => 'superieur', + 'active' => true + ), + array( + 'name' => ['fr' => 'Secondaire supérieur (CESS)'], + 'slug' => 'cess', + 'active' => true + ), + array( + 'name' => ['fr' => 'Secondaire deuxième degré ou inférieur (C2D)'], + 'slug' => 'c2d', + 'active' => true + ), + array( + 'name' => ['fr' => 'Primaire'], + 'slug' => 'low', + 'active' => true + ), + array( + 'name' => ['fr' => 'Aucun diplome'], + 'slug' => 'no', + 'active' => true + ) + ] + + )) + ->setOrdering(20) + ->setCustomFieldsGroup($reportEducation) + ; + $manager->persist($educationLevel); + + + + } +} \ No newline at end of file diff --git a/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomFieldsGroup.php b/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomFieldsGroup.php new file mode 100644 index 000000000..ee45cc728 --- /dev/null +++ b/src/Bundle/ChillReport/DataFixtures/ORM/LoadCustomFieldsGroup.php @@ -0,0 +1,89 @@ + + * + * 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\ReportBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup; + +/** + * Load CustomFieldsGroup for Report into database + */ +class LoadCustomFieldsGroup extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 15000; + } + + public function load(ObjectManager $manager) + { + echo "loading customFieldsGroup...\n"; + + $report = $this->createReport($manager, + array('fr' => 'Situation de logement'), + ['summary_fields' => ['has_logement', 'house-desc']]); + $this->addReference( + 'cf_group_report_logement', + $report + ); + + $report = $this->createReport($manager, array('fr' => 'Alphabétisme')); + $this->addReference('cf_group_report_education', $report); + + for($i=0; $i <= 3; $i++) { + + $report = $this->createReport($manager, array('fr' => 'ZZ Rapport aléatoire '.$i)); + + $this->addReference('cf_group_report_'.$i, $report); + } + + + + $manager->flush(); + } + + /** + * create a report and persist in the db + * + * @param ObjectManager $manager + * @param array $name + * @return CustomFieldsGroup + */ + private function createReport( + ObjectManager $manager, + array $name, + array $options = array()) + { + echo $name['fr']." \n"; + + $cFGroup = (new CustomFieldsGroup()) + ->setName($name) + ->setEntity('Chill\ReportBundle\Entity\Report') + ->setOptions($options); + + $manager->persist($cFGroup); + + return $cFGroup; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillReport/DataFixtures/ORM/LoadReportACL.php b/src/Bundle/ChillReport/DataFixtures/ORM/LoadReportACL.php new file mode 100644 index 000000000..f2a97c16d --- /dev/null +++ b/src/Bundle/ChillReport/DataFixtures/ORM/LoadReportACL.php @@ -0,0 +1,88 @@ + + * + * 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\ReportBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; +use Chill\MainBundle\DataFixtures\ORM\LoadScopes; + +/** + * Add a role CHILL_REPORT_UPDATE & CHILL_REPORT_CREATE for all groups except administrative, + * and a role CHILL_REPORT_SEE for administrative + * + * @author Julien Fastré + */ +class LoadReportACL extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 14999; + } + + + public function load(ObjectManager $manager) + { + foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { + $permissionsGroup = $this->getReference($permissionsGroupRef); + printf("processing permission group %s \n", $permissionsGroup->getName()); + foreach (LoadScopes::$references as $scopeRef){ + $scope = $this->getReference($scopeRef); + printf("processing scope %s \n", $scope->getName()['en']); + //create permission group + switch ($permissionsGroup->getName()) { + case 'social': + if ($scope->getName()['en'] === 'administrative') { + printf("denying power on administrative \n"); + break 2; // we do not want any power on administrative + } + break; + case 'administrative': + case 'direction': + if (in_array($scope->getName()['en'], array('administrative', 'social'))) { + printf("denying power on %s\n", $scope->getName()['en']); + break 2; // we do not want any power on social or administrative + } + break; + } + + printf("Adding CHILL_REPORT_UPDATE & CHILL_REPORT_CREATE to %s " + . "permission group, scope '%s' \n", + $permissionsGroup->getName(), $scope->getName()['en']); + $roleScopeUpdate = (new RoleScope()) + ->setRole('CHILL_REPORT_UPDATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeUpdate); + $roleScopeCreate = (new RoleScope()) + ->setRole('CHILL_REPORT_CREATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeCreate); + $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeCreate); + } + + } + + $manager->flush(); + } + +} diff --git a/src/Bundle/ChillReport/DataFixtures/ORM/LoadReports.php b/src/Bundle/ChillReport/DataFixtures/ORM/LoadReports.php new file mode 100644 index 000000000..4e9ea028e --- /dev/null +++ b/src/Bundle/ChillReport/DataFixtures/ORM/LoadReports.php @@ -0,0 +1,240 @@ + + * + * 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\ReportBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Chill\ReportBundle\Entity\Report; +use Chill\MainBundle\DataFixtures\ORM\LoadUsers; +use Faker\Factory as FakerFactory; +use Chill\CustomFieldsBundle\Entity\CustomField; +use Chill\MainBundle\DataFixtures\ORM\LoadScopes; + +/** + * Load reports into DB + * + * @author Julien Fastré + */ +class LoadReports extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface +{ + use \Symfony\Component\DependencyInjection\ContainerAwareTrait; + + /** + * + * @var \Faker\Generator + */ + private $faker; + + public function __construct() + { + $this->faker = FakerFactory::create('fr_FR'); + } + + public function getOrder() + { + return 15002; + } + + public function load(ObjectManager $manager) + { + $this->createExpected($manager); + + //create random 2 times, to allow multiple report on some people + $this->createRandom($manager, 90); + $this->createRandom($manager, 30); + + $manager->flush(); + } + + private function createRandom(ObjectManager $manager, $percentage) + { + $people = $this->getPeopleRandom($percentage); + + foreach ($people as $person) { + //create a report, set logement or education report + $report = (new Report()) + ->setPerson($person) + ->setCFGroup(rand(0,10) > 5 ? + $this->getReference('cf_group_report_logement') : + $this->getReference('cf_group_report_education') + ) + ->setScope($this->getScopeRandom()) + ; + $this->fillReport($report); + $manager->persist($report); + } + } + + private function createExpected(ObjectManager $manager) + { + $charline = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillPersonBundle:Person') + ->findOneBy(array('lastName' => 'Charline')) + ; + + $report = (new Report()) + ->setPerson($charline) + ->setCFGroup($this->getReference('cf_group_report_logement')) + ->setDate(new \DateTime('2015-01-05')) + ->setScope($this->getReference('scope_social')) + ; + $this->fillReport($report); + + $manager->persist($report); + } + + /** + * + * @return \Chill\MainBundle\Entity\Scope + */ + private function getScopeRandom() + { + $ref = LoadScopes::$references[array_rand(LoadScopes::$references)]; + return $this->getReference($ref); + } + + private function getPeopleRandom($percentage) + { + $people = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillPersonBundle:Person') + ->findAll() + ; + + //keep only a part ($percentage) of the people + $selectedPeople = array(); + foreach($people as $person) { + if (rand(0,100) < $percentage) { + $selectedPeople[] = $person; + } + } + + return $selectedPeople; + } + + private function fillReport(Report $report) + { + //setUser + $usernameRef = array_rand(LoadUsers::$refs); + $report->setUser( + $this->getReference($usernameRef) + ); + + //set date if null + if ($report->getDate() === NULL) { + //set date. 30% of the dates are 2015-05-01 + $expectedDate = new \DateTime('2015-01-05'); + if (rand(0,100) < 30) { + $report->setDate($expectedDate); + } else { + $report->setDate($this->faker->dateTimeBetween('-1 year', 'now') + ->setTime(0, 0, 0)); + } + } + + //fill data + $datas = array(); + foreach ($report->getCFGroup()->getCustomFields() as $field) { + switch ($field->getType()) { + case 'title' : + $datas[$field->getSlug()] = array(); + break; + case 'choice' : + $datas[$field->getSlug()] = $this->getRandomChoice($field); + break; + case 'text' : + $datas[$field->getSlug()] = $this->faker->realText($field->getOptions()['maxLength']); + break; + } + } + $report->setCFData($datas); + + return $report; + } + + /** + * pick a random choice + * + * @param CustomField $field + * @return string[]|string the array of slug if multiple, a single slug otherwise + */ + private function getRandomChoice(CustomField $field) + { + $choices = $field->getOptions()['choices']; + $multiple = $field->getOptions()['multiple']; + $other = $field->getOptions()['other']; + + //add other if allowed + if($other) { + $choices[] = array('slug' => '_other'); + } + + //initialize results + $picked = array(); + + if ($multiple) { + $numberSelected = rand(1, count($choices) -1); + for ($i = 0; $i < $numberSelected; $i++) { + $picked[] = $this->pickChoice($choices); + } + + if ($other) { + $result = array("_other" => NULL, "_choices" => $picked); + + if (in_array('_other', $picked)) { + $result['_other'] = $this->faker->realText(70); + } + + return $result; + } + + } else { + $picked = $this->pickChoice($choices); + + if ($other) { + $result = array('_other' => NULL, '_choices' => $picked); + + if ($picked === '_other') { + $result['_other'] = $this->faker->realText(70); + } + + return $result; + } + } + + + } + + /** + * pick a choice within a 'choices' options (for choice type) + * + * @param array $choices + * @return the slug of the selected choice + */ + private function pickChoice(array $choices) + { + return $choices[array_rand($choices)]['slug']; + } + + + +} diff --git a/src/Bundle/ChillReport/DependencyInjection/ChillReportExtension.php b/src/Bundle/ChillReport/DependencyInjection/ChillReportExtension.php new file mode 100644 index 000000000..359017020 --- /dev/null +++ b/src/Bundle/ChillReport/DependencyInjection/ChillReportExtension.php @@ -0,0 +1,106 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); + $loader->load('services.yaml'); + $loader->load('services/fixtures.yaml'); + $loader->load('services/export.yaml'); + $loader->load('services/controller.yaml'); + } + + /** + * Declare the entity Report, as a customizable entity (can add custom fields) + * + * @param ContainerBuilder $container + */ + public function declareReportAsCustomizable(ContainerBuilder $container) + { + $bundles = $container->getParameter('kernel.bundles'); + if (!isset($bundles['ChillCustomFieldsBundle'])) { + throw new MissingBundleException('ChillCustomFieldsBundle'); + } + + $container->prependExtensionConfig('chill_custom_fields', + array('customizables_entities' => + array( + array( + 'class' => 'Chill\ReportBundle\Entity\Report', + 'name' => 'ReportEntity', + 'options' => array( + 'summary_fields' => array( + 'form_type' => LinkedCustomFieldsType::class, + 'form_options' => + [ + 'multiple' => true, + 'expanded' => false + ] + ) + )) + ) + ) + ); + } + + /** + * declare routes from report bundle + * + * @param ContainerBuilder $container + */ + private function declareRouting(ContainerBuilder $container) + { + $container->prependExtensionConfig('chill_main', array( + 'routing' => array( + 'resources' => array( + '@ChillReportBundle/config/routes.yaml' + ) + ) + )); + } + + protected function prependRoleHierarchy(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + 'CHILL_REPORT_UPDATE' => array('CHILL_REPORT_SEE'), + 'CHILL_REPORT_CREATE' => array('CHILL_REPORT_SEE') + ) + )); + } + + /** + * {@inheritdoc} + * + * @param ContainerBuilder $container + */ + public function prepend(ContainerBuilder $container) + { + $this->declareReportAsCustomizable($container); + $this->declareRouting($container); + $this->prependRoleHierarchy($container); + } + +} diff --git a/src/Bundle/ChillReport/DependencyInjection/Configuration.php b/src/Bundle/ChillReport/DependencyInjection/Configuration.php new file mode 100644 index 000000000..2b3b75672 --- /dev/null +++ b/src/Bundle/ChillReport/DependencyInjection/Configuration.php @@ -0,0 +1,29 @@ +getRootNode('chill_report'); + + // Here you should define the parameters that are allowed to + // configure your bundle. See the documentation linked above for + // more information on that topic. + + return $treeBuilder; + } +} diff --git a/src/Bundle/ChillReport/Entity/Report.php b/src/Bundle/ChillReport/Entity/Report.php new file mode 100644 index 000000000..fa81d9283 --- /dev/null +++ b/src/Bundle/ChillReport/Entity/Report.php @@ -0,0 +1,245 @@ + + * + * 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\ReportBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Center; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Entity\HasCenterInterface; +use Chill\MainBundle\Entity\HasScopeInterface; +use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup; + +/** + * Class Report + * + * @package Chill\ReportBundle\Entity + * @ORM\Entity() + * @ORM\Table(name="report") + * @ORM\HasLifecycleCallbacks() + */ +class Report implements HasCenterInterface, HasScopeInterface +{ + /** + * @var integer + * + * @ORM\Id + * @ORM\Column(name="id", type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var User + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") + */ + private $user; + + /** + * @var Person + * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person") + */ + private $person; + + /** + * @var \DateTime + * @ORM\Column(type="datetime") + */ + private $date; + + /** + * @var Scope + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Scope") + */ + private $scope; + + /** + * @var array + * @ORM\Column(type="json_array") + */ + private $cFData; + + /** + * @var CustomFieldsGroup + * @ORM\ManyToOne( + * targetEntity="Chill\CustomFieldsBundle\Entity\CustomFieldsGroup") + */ + private $cFGroup; + + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set user + * + * @param User $user + * @return Report + */ + public function setUser(User $user) + { + $this->user = $user; + + return $this; + } + + /** + * Get user + * + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * Set person + * + * @param Person $person + * @return Report + */ + public function setPerson(Person $person) + { + $this->person = $person; + + return $this; + } + + /** + * Get person + * + * @return Person + */ + public function getPerson() + { + return $this->person; + } + + /** + * Set date + * + * @param \DateTime $date + * @return Report + */ + public function setDate($date) + { + $this->date = $date; + + return $this; + } + + /** + * Get date + * + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } + + /** + * Set scope + * + * @param string $scope + * @return Report + */ + public function setScope(Scope $scope) + { + $this->scope = $scope; + + return $this; + } + + /** + * Get scope + * + * @return Scope + */ + public function getScope() + { + return $this->scope; + } + + /** + * Set cFData + * + * @param array $cFData + * @return Report + */ + public function setCFData(array $cFData) + { + $this->cFData = $cFData; + + return $this; + } + + /** + * Get cFData + * + * @return array + */ + public function getCFData() + { + return $this->cFData; + } + + /** + * Set cFGroup + * + * @param CustomFieldsGroup $cFGroup + * @return Report + */ + public function setCFGroup(CustomFieldsGroup $cFGroup) + { + $this->cFGroup = $cFGroup; + + return $this; + } + + /** + * Get cFGroup + * + * @return CustomFieldsGroup + */ + public function getCFGroup() + { + return $this->cFGroup; + } + + /** + * @return Center + */ + public function getCenter() + { + return $this->person->getCenter(); + } + +} diff --git a/src/Bundle/ChillReport/Export/Export/ReportList.php b/src/Bundle/ChillReport/Export/Export/ReportList.php new file mode 100644 index 000000000..583db8f4b --- /dev/null +++ b/src/Bundle/ChillReport/Export/Export/ReportList.php @@ -0,0 +1,556 @@ + + */ +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(); + + // process fields which are not custom fields + foreach ($this->fields as $f) { + // do not add fields which are not selected + if (!\in_array($f, $data['fields'])) { + continue; + } + + // add a column to the query for each field + 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"); + } + } + + } + + // process fields which are custom fields + foreach ($this->getCustomFields() as $cf) { + // do not add custom fields which are not selected + if (!\in_array($cf->getSlug(), $data['fields'])) { + continue; + } + + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); + + // if is multiple, split into multiple columns + 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 { + // not multiple, add a single column + $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/src/Bundle/ChillReport/Export/Export/ReportListProvider.php b/src/Bundle/ChillReport/Export/Export/ReportListProvider.php new file mode 100644 index 000000000..e7b8a2a79 --- /dev/null +++ b/src/Bundle/ChillReport/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/src/Bundle/ChillReport/Export/Filter/ReportDateFilter.php b/src/Bundle/ChillReport/Export/Filter/ReportDateFilter.php new file mode 100644 index 000000000..379d9ccda --- /dev/null +++ b/src/Bundle/ChillReport/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/src/Bundle/ChillReport/Form/ReportType.php b/src/Bundle/ChillReport/Form/ReportType.php new file mode 100644 index 000000000..d804dd6b8 --- /dev/null +++ b/src/Bundle/ChillReport/Form/ReportType.php @@ -0,0 +1,120 @@ + + * + * 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\ReportBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Chill\MainBundle\Form\Type\AppendScopeChoiceTypeTrait; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\Persistence\ObjectManager; +use Chill\CustomFieldsBundle\Form\Type\CustomFieldType; + +class ReportType extends AbstractType +{ + use AppendScopeChoiceTypeTrait; + + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * + * @var \Doctrine\Persistence\ObjectManager + */ + protected $om; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + public function __construct(AuthorizationHelper $helper, + TokenStorageInterface $tokenStorage, + TranslatableStringHelper $translatableStringHelper, + ObjectManager $om) + { + $this->authorizationHelper = $helper; + $this->user = $tokenStorage->getToken()->getUser(); + $this->translatableStringHelper = $translatableStringHelper; + $this->om = $om; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('user') + ->add('date', DateType::class, + array('required' => true, 'widget' => 'single_text', 'format' => 'dd-MM-yyyy')) + ->add('cFData', CustomFieldType::class, + array('attr' => array('class' => 'cf-fields'), + 'group' => $options['cFGroup'])) + ; + + $this->appendScopeChoices($builder, $options['role'], $options['center'], + $this->user, $this->authorizationHelper, + $this->translatableStringHelper, + $this->om); + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'Chill\ReportBundle\Entity\Report' + )); + + $resolver->setRequired(array( + 'cFGroup', + )); + + $resolver->setAllowedTypes('cFGroup', 'Chill\CustomFieldsBundle\Entity\CustomFieldsGroup'); + + $this->appendScopeChoicesOptions($resolver); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'chill_reportbundle_report'; + } +} diff --git a/src/Bundle/ChillReport/LICENSE b/src/Bundle/ChillReport/LICENSE new file mode 100644 index 000000000..dba13ed2d --- /dev/null +++ b/src/Bundle/ChillReport/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/src/Bundle/ChillReport/README.md b/src/Bundle/ChillReport/README.md new file mode 100644 index 000000000..992414664 --- /dev/null +++ b/src/Bundle/ChillReport/README.md @@ -0,0 +1,9 @@ +ChillReport +=========== + +The bundle for reports. This is a bundle of the Chill project. + +Documentation & installation +---------------------------- + +Read documentation here : http://chill.readthedocs.org \ No newline at end of file diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/.DS_Store b/src/Bundle/ChillReport/Resources/test/Fixtures/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/src/Bundle/ChillReport/Resources/test/Fixtures/.DS_Store differ diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/AppKernel.php b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/AppKernel.php new file mode 100644 index 000000000..4222efe69 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/AppKernel.php @@ -0,0 +1,46 @@ +load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml'); + } + + /** + * @return string + */ + public function getCacheDir() + { + return sys_get_temp_dir().'/ChillReportBundle/cache'; + } + + /** + * @return string + */ + public function getLogDir() + { + return sys_get_temp_dir().'/ChillReportBundle/logs'; + } +} diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/autoload.php b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/autoload.php new file mode 100644 index 000000000..39dc0e2ba --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/autoload.php @@ -0,0 +1,11 @@ +parameters = $parameters; +} +public function all() +{ +return $this->parameters; +} +public function keys() +{ +return array_keys($this->parameters); +} +public function replace(array $parameters = array()) +{ +$this->parameters = $parameters; +} +public function add(array $parameters = array()) +{ +$this->parameters = array_replace($this->parameters, $parameters); +} +public function get($key, $default = null, $deep = false) +{ +if ($deep) { +@trigger_error('Using paths to find deeper items in '.__METHOD__.' is deprecated since version 2.8 and will be removed in 3.0. Filter the returned value in your own code instead.', E_USER_DEPRECATED); +} +if (!$deep || false === $pos = strpos($key,'[')) { +return array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; +} +$root = substr($key, 0, $pos); +if (!array_key_exists($root, $this->parameters)) { +return $default; +} +$value = $this->parameters[$root]; +$currentKey = null; +for ($i = $pos, $c = strlen($key); $i < $c; ++$i) { +$char = $key[$i]; +if ('['=== $char) { +if (null !== $currentKey) { +throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "[" at position %d.', $i)); +} +$currentKey =''; +} elseif (']'=== $char) { +if (null === $currentKey) { +throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "]" at position %d.', $i)); +} +if (!is_array($value) || !array_key_exists($currentKey, $value)) { +return $default; +} +$value = $value[$currentKey]; +$currentKey = null; +} else { +if (null === $currentKey) { +throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "%s" at position %d.', $char, $i)); +} +$currentKey .= $char; +} +} +if (null !== $currentKey) { +throw new \InvalidArgumentException('Malformed path. Path must end with "]".'); +} +return $value; +} +public function set($key, $value) +{ +$this->parameters[$key] = $value; +} +public function has($key) +{ +return array_key_exists($key, $this->parameters); +} +public function remove($key) +{ +unset($this->parameters[$key]); +} +public function getAlpha($key, $default ='', $deep = false) +{ +return preg_replace('/[^[:alpha:]]/','', $this->get($key, $default, $deep)); +} +public function getAlnum($key, $default ='', $deep = false) +{ +return preg_replace('/[^[:alnum:]]/','', $this->get($key, $default, $deep)); +} +public function getDigits($key, $default ='', $deep = false) +{ +return str_replace(array('-','+'),'', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT, array(), $deep)); +} +public function getInt($key, $default = 0, $deep = false) +{ +return (int) $this->get($key, $default, $deep); +} +public function getBoolean($key, $default = false, $deep = false) +{ +return $this->filter($key, $default, FILTER_VALIDATE_BOOLEAN, array(), $deep); +} +public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = array(), $deep = false) +{ +static $filters = null; +if (null === $filters) { +foreach (filter_list() as $tmp) { +$filters[filter_id($tmp)] = 1; +} +} +if (is_bool($filter) || !isset($filters[$filter]) || is_array($deep)) { +@trigger_error('Passing the $deep boolean as 3rd argument to the '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Remove it altogether as the $deep argument will be removed in 3.0.', E_USER_DEPRECATED); +$tmp = $deep; +$deep = $filter; +$filter = $options; +$options = $tmp; +} +$value = $this->get($key, $default, $deep); +if (!is_array($options) && $options) { +$options = array('flags'=> $options); +} +if (is_array($value) && !isset($options['flags'])) { +$options['flags'] = FILTER_REQUIRE_ARRAY; +} +return filter_var($value, $filter, $options); +} +public function getIterator() +{ +return new \ArrayIterator($this->parameters); +} +public function count() +{ +return count($this->parameters); +} +} +} +namespace Symfony\Component\HttpFoundation +{ +class HeaderBag implements \IteratorAggregate, \Countable +{ +protected $headers = array(); +protected $cacheControl = array(); +public function __construct(array $headers = array()) +{ +foreach ($headers as $key => $values) { +$this->set($key, $values); +} +} +public function __toString() +{ +if (!$this->headers) { +return''; +} +$max = max(array_map('strlen', array_keys($this->headers))) + 1; +$content =''; +ksort($this->headers); +foreach ($this->headers as $name => $values) { +$name = implode('-', array_map('ucfirst', explode('-', $name))); +foreach ($values as $value) { +$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); +} +} +return $content; +} +public function all() +{ +return $this->headers; +} +public function keys() +{ +return array_keys($this->headers); +} +public function replace(array $headers = array()) +{ +$this->headers = array(); +$this->add($headers); +} +public function add(array $headers) +{ +foreach ($headers as $key => $values) { +$this->set($key, $values); +} +} +public function get($key, $default = null, $first = true) +{ +$key = str_replace('_','-', strtolower($key)); +if (!array_key_exists($key, $this->headers)) { +if (null === $default) { +return $first ? null : array(); +} +return $first ? $default : array($default); +} +if ($first) { +return count($this->headers[$key]) ? $this->headers[$key][0] : $default; +} +return $this->headers[$key]; +} +public function set($key, $values, $replace = true) +{ +$key = str_replace('_','-', strtolower($key)); +$values = array_values((array) $values); +if (true === $replace || !isset($this->headers[$key])) { +$this->headers[$key] = $values; +} else { +$this->headers[$key] = array_merge($this->headers[$key], $values); +} +if ('cache-control'=== $key) { +$this->cacheControl = $this->parseCacheControl($values[0]); +} +} +public function has($key) +{ +return array_key_exists(str_replace('_','-', strtolower($key)), $this->headers); +} +public function contains($key, $value) +{ +return in_array($value, $this->get($key, null, false)); +} +public function remove($key) +{ +$key = str_replace('_','-', strtolower($key)); +unset($this->headers[$key]); +if ('cache-control'=== $key) { +$this->cacheControl = array(); +} +} +public function getDate($key, \DateTime $default = null) +{ +if (null === $value = $this->get($key)) { +return $default; +} +if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) { +throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value)); +} +return $date; +} +public function addCacheControlDirective($key, $value = true) +{ +$this->cacheControl[$key] = $value; +$this->set('Cache-Control', $this->getCacheControlHeader()); +} +public function hasCacheControlDirective($key) +{ +return array_key_exists($key, $this->cacheControl); +} +public function getCacheControlDirective($key) +{ +return array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null; +} +public function removeCacheControlDirective($key) +{ +unset($this->cacheControl[$key]); +$this->set('Cache-Control', $this->getCacheControlHeader()); +} +public function getIterator() +{ +return new \ArrayIterator($this->headers); +} +public function count() +{ +return count($this->headers); +} +protected function getCacheControlHeader() +{ +$parts = array(); +ksort($this->cacheControl); +foreach ($this->cacheControl as $key => $value) { +if (true === $value) { +$parts[] = $key; +} else { +if (preg_match('#[^a-zA-Z0-9._-]#', $value)) { +$value ='"'.$value.'"'; +} +$parts[] = "$key=$value"; +} +} +return implode(', ', $parts); +} +protected function parseCacheControl($header) +{ +$cacheControl = array(); +preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER); +foreach ($matches as $match) { +$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true); +} +return $cacheControl; +} +} +} +namespace Symfony\Component\HttpFoundation +{ +use Symfony\Component\HttpFoundation\File\UploadedFile; +class FileBag extends ParameterBag +{ +private static $fileKeys = array('error','name','size','tmp_name','type'); +public function __construct(array $parameters = array()) +{ +$this->replace($parameters); +} +public function replace(array $files = array()) +{ +$this->parameters = array(); +$this->add($files); +} +public function set($key, $value) +{ +if (!is_array($value) && !$value instanceof UploadedFile) { +throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); +} +parent::set($key, $this->convertFileInformation($value)); +} +public function add(array $files = array()) +{ +foreach ($files as $key => $file) { +$this->set($key, $file); +} +} +protected function convertFileInformation($file) +{ +if ($file instanceof UploadedFile) { +return $file; +} +$file = $this->fixPhpFilesArray($file); +if (is_array($file)) { +$keys = array_keys($file); +sort($keys); +if ($keys == self::$fileKeys) { +if (UPLOAD_ERR_NO_FILE == $file['error']) { +$file = null; +} else { +$file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']); +} +} else { +$file = array_map(array($this,'convertFileInformation'), $file); +} +} +return $file; +} +protected function fixPhpFilesArray($data) +{ +if (!is_array($data)) { +return $data; +} +$keys = array_keys($data); +sort($keys); +if (self::$fileKeys != $keys || !isset($data['name']) || !is_array($data['name'])) { +return $data; +} +$files = $data; +foreach (self::$fileKeys as $k) { +unset($files[$k]); +} +foreach ($data['name'] as $key => $name) { +$files[$key] = $this->fixPhpFilesArray(array('error'=> $data['error'][$key],'name'=> $name,'type'=> $data['type'][$key],'tmp_name'=> $data['tmp_name'][$key],'size'=> $data['size'][$key], +)); +} +return $files; +} +} +} +namespace Symfony\Component\HttpFoundation +{ +class ServerBag extends ParameterBag +{ +public function getHeaders() +{ +$headers = array(); +$contentHeaders = array('CONTENT_LENGTH'=> true,'CONTENT_MD5'=> true,'CONTENT_TYPE'=> true); +foreach ($this->parameters as $key => $value) { +if (0 === strpos($key,'HTTP_')) { +$headers[substr($key, 5)] = $value; +} +elseif (isset($contentHeaders[$key])) { +$headers[$key] = $value; +} +} +if (isset($this->parameters['PHP_AUTH_USER'])) { +$headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; +$headers['PHP_AUTH_PW'] = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] :''; +} else { +$authorizationHeader = null; +if (isset($this->parameters['HTTP_AUTHORIZATION'])) { +$authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; +} elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { +$authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; +} +if (null !== $authorizationHeader) { +if (0 === stripos($authorizationHeader,'basic ')) { +$exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); +if (count($exploded) == 2) { +list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; +} +} elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader,'digest '))) { +$headers['PHP_AUTH_DIGEST'] = $authorizationHeader; +$this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; +} elseif (0 === stripos($authorizationHeader,'bearer ')) { +$headers['AUTHORIZATION'] = $authorizationHeader; +} +} +} +if (isset($headers['AUTHORIZATION'])) { +return $headers; +} +if (isset($headers['PHP_AUTH_USER'])) { +$headers['AUTHORIZATION'] ='Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']); +} elseif (isset($headers['PHP_AUTH_DIGEST'])) { +$headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; +} +return $headers; +} +} +} +namespace Symfony\Component\HttpFoundation +{ +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +class Request +{ +const HEADER_FORWARDED ='forwarded'; +const HEADER_CLIENT_IP ='client_ip'; +const HEADER_CLIENT_HOST ='client_host'; +const HEADER_CLIENT_PROTO ='client_proto'; +const HEADER_CLIENT_PORT ='client_port'; +const METHOD_HEAD ='HEAD'; +const METHOD_GET ='GET'; +const METHOD_POST ='POST'; +const METHOD_PUT ='PUT'; +const METHOD_PATCH ='PATCH'; +const METHOD_DELETE ='DELETE'; +const METHOD_PURGE ='PURGE'; +const METHOD_OPTIONS ='OPTIONS'; +const METHOD_TRACE ='TRACE'; +const METHOD_CONNECT ='CONNECT'; +protected static $trustedProxies = array(); +protected static $trustedHostPatterns = array(); +protected static $trustedHosts = array(); +protected static $trustedHeaders = array( +self::HEADER_FORWARDED =>'FORWARDED', +self::HEADER_CLIENT_IP =>'X_FORWARDED_FOR', +self::HEADER_CLIENT_HOST =>'X_FORWARDED_HOST', +self::HEADER_CLIENT_PROTO =>'X_FORWARDED_PROTO', +self::HEADER_CLIENT_PORT =>'X_FORWARDED_PORT', +); +protected static $httpMethodParameterOverride = false; +public $attributes; +public $request; +public $query; +public $server; +public $files; +public $cookies; +public $headers; +protected $content; +protected $languages; +protected $charsets; +protected $encodings; +protected $acceptableContentTypes; +protected $pathInfo; +protected $requestUri; +protected $baseUrl; +protected $basePath; +protected $method; +protected $format; +protected $session; +protected $locale; +protected $defaultLocale ='en'; +protected static $formats; +protected static $requestFactory; +private $isForwardedValid = true; +private static $forwardedParams = array( +self::HEADER_CLIENT_IP =>'for', +self::HEADER_CLIENT_HOST =>'host', +self::HEADER_CLIENT_PROTO =>'proto', +self::HEADER_CLIENT_PORT =>'host', +); +public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) +{ +$this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); +} +public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) +{ +$this->request = new ParameterBag($request); +$this->query = new ParameterBag($query); +$this->attributes = new ParameterBag($attributes); +$this->cookies = new ParameterBag($cookies); +$this->files = new FileBag($files); +$this->server = new ServerBag($server); +$this->headers = new HeaderBag($this->server->getHeaders()); +$this->content = $content; +$this->languages = null; +$this->charsets = null; +$this->encodings = null; +$this->acceptableContentTypes = null; +$this->pathInfo = null; +$this->requestUri = null; +$this->baseUrl = null; +$this->basePath = null; +$this->method = null; +$this->format = null; +} +public static function createFromGlobals() +{ +$server = $_SERVER; +if ('cli-server'=== PHP_SAPI) { +if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) { +$server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH']; +} +if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) { +$server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']; +} +} +$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server); +if (0 === strpos($request->headers->get('CONTENT_TYPE'),'application/x-www-form-urlencoded') +&& in_array(strtoupper($request->server->get('REQUEST_METHOD','GET')), array('PUT','DELETE','PATCH')) +) { +parse_str($request->getContent(), $data); +$request->request = new ParameterBag($data); +} +return $request; +} +public static function create($uri, $method ='GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null) +{ +$server = array_replace(array('SERVER_NAME'=>'localhost','SERVER_PORT'=> 80,'HTTP_HOST'=>'localhost','HTTP_USER_AGENT'=>'Symfony/2.X','HTTP_ACCEPT'=>'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','HTTP_ACCEPT_LANGUAGE'=>'en-us,en;q=0.5','HTTP_ACCEPT_CHARSET'=>'ISO-8859-1,utf-8;q=0.7,*;q=0.7','REMOTE_ADDR'=>'127.0.0.1','SCRIPT_NAME'=>'','SCRIPT_FILENAME'=>'','SERVER_PROTOCOL'=>'HTTP/1.1','REQUEST_TIME'=> time(), +), $server); +$server['PATH_INFO'] =''; +$server['REQUEST_METHOD'] = strtoupper($method); +$components = parse_url($uri); +if (isset($components['host'])) { +$server['SERVER_NAME'] = $components['host']; +$server['HTTP_HOST'] = $components['host']; +} +if (isset($components['scheme'])) { +if ('https'=== $components['scheme']) { +$server['HTTPS'] ='on'; +$server['SERVER_PORT'] = 443; +} else { +unset($server['HTTPS']); +$server['SERVER_PORT'] = 80; +} +} +if (isset($components['port'])) { +$server['SERVER_PORT'] = $components['port']; +$server['HTTP_HOST'] = $server['HTTP_HOST'].':'.$components['port']; +} +if (isset($components['user'])) { +$server['PHP_AUTH_USER'] = $components['user']; +} +if (isset($components['pass'])) { +$server['PHP_AUTH_PW'] = $components['pass']; +} +if (!isset($components['path'])) { +$components['path'] ='/'; +} +switch (strtoupper($method)) { +case'POST': +case'PUT': +case'DELETE': +if (!isset($server['CONTENT_TYPE'])) { +$server['CONTENT_TYPE'] ='application/x-www-form-urlencoded'; +} +case'PATCH': +$request = $parameters; +$query = array(); +break; +default: +$request = array(); +$query = $parameters; +break; +} +$queryString =''; +if (isset($components['query'])) { +parse_str(html_entity_decode($components['query']), $qs); +if ($query) { +$query = array_replace($qs, $query); +$queryString = http_build_query($query,'','&'); +} else { +$query = $qs; +$queryString = $components['query']; +} +} elseif ($query) { +$queryString = http_build_query($query,'','&'); +} +$server['REQUEST_URI'] = $components['path'].(''!== $queryString ?'?'.$queryString :''); +$server['QUERY_STRING'] = $queryString; +return self::createRequestFromFactory($query, $request, array(), $cookies, $files, $server, $content); +} +public static function setFactory($callable) +{ +self::$requestFactory = $callable; +} +public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) +{ +$dup = clone $this; +if ($query !== null) { +$dup->query = new ParameterBag($query); +} +if ($request !== null) { +$dup->request = new ParameterBag($request); +} +if ($attributes !== null) { +$dup->attributes = new ParameterBag($attributes); +} +if ($cookies !== null) { +$dup->cookies = new ParameterBag($cookies); +} +if ($files !== null) { +$dup->files = new FileBag($files); +} +if ($server !== null) { +$dup->server = new ServerBag($server); +$dup->headers = new HeaderBag($dup->server->getHeaders()); +} +$dup->languages = null; +$dup->charsets = null; +$dup->encodings = null; +$dup->acceptableContentTypes = null; +$dup->pathInfo = null; +$dup->requestUri = null; +$dup->baseUrl = null; +$dup->basePath = null; +$dup->method = null; +$dup->format = null; +if (!$dup->get('_format') && $this->get('_format')) { +$dup->attributes->set('_format', $this->get('_format')); +} +if (!$dup->getRequestFormat(null)) { +$dup->setRequestFormat($this->getRequestFormat(null)); +} +return $dup; +} +public function __clone() +{ +$this->query = clone $this->query; +$this->request = clone $this->request; +$this->attributes = clone $this->attributes; +$this->cookies = clone $this->cookies; +$this->files = clone $this->files; +$this->server = clone $this->server; +$this->headers = clone $this->headers; +} +public function __toString() +{ +try { +$content = $this->getContent(); +} catch (\LogicException $e) { +return trigger_error($e, E_USER_ERROR); +} +return +sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". +$this->headers."\r\n". +$content; +} +public function overrideGlobals() +{ +$this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), null,'&'))); +$_GET = $this->query->all(); +$_POST = $this->request->all(); +$_SERVER = $this->server->all(); +$_COOKIE = $this->cookies->all(); +foreach ($this->headers->all() as $key => $value) { +$key = strtoupper(str_replace('-','_', $key)); +if (in_array($key, array('CONTENT_TYPE','CONTENT_LENGTH'))) { +$_SERVER[$key] = implode(', ', $value); +} else { +$_SERVER['HTTP_'.$key] = implode(', ', $value); +} +} +$request = array('g'=> $_GET,'p'=> $_POST,'c'=> $_COOKIE); +$requestOrder = ini_get('request_order') ?: ini_get('variables_order'); +$requestOrder = preg_replace('#[^cgp]#','', strtolower($requestOrder)) ?:'gp'; +$_REQUEST = array(); +foreach (str_split($requestOrder) as $order) { +$_REQUEST = array_merge($_REQUEST, $request[$order]); +} +} +public static function setTrustedProxies(array $proxies) +{ +self::$trustedProxies = $proxies; +} +public static function getTrustedProxies() +{ +return self::$trustedProxies; +} +public static function setTrustedHosts(array $hostPatterns) +{ +self::$trustedHostPatterns = array_map(function ($hostPattern) { +return sprintf('#%s#i', $hostPattern); +}, $hostPatterns); +self::$trustedHosts = array(); +} +public static function getTrustedHosts() +{ +return self::$trustedHostPatterns; +} +public static function setTrustedHeaderName($key, $value) +{ +if (!array_key_exists($key, self::$trustedHeaders)) { +throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key)); +} +self::$trustedHeaders[$key] = $value; +} +public static function getTrustedHeaderName($key) +{ +if (!array_key_exists($key, self::$trustedHeaders)) { +throw new \InvalidArgumentException(sprintf('Unable to get the trusted header name for key "%s".', $key)); +} +return self::$trustedHeaders[$key]; +} +public static function normalizeQueryString($qs) +{ +if (''== $qs) { +return''; +} +$parts = array(); +$order = array(); +foreach (explode('&', $qs) as $param) { +if (''=== $param ||'='=== $param[0]) { +continue; +} +$keyValuePair = explode('=', $param, 2); +$parts[] = isset($keyValuePair[1]) ? +rawurlencode(urldecode($keyValuePair[0])).'='.rawurlencode(urldecode($keyValuePair[1])) : +rawurlencode(urldecode($keyValuePair[0])); +$order[] = urldecode($keyValuePair[0]); +} +array_multisort($order, SORT_ASC, $parts); +return implode('&', $parts); +} +public static function enableHttpMethodParameterOverride() +{ +self::$httpMethodParameterOverride = true; +} +public static function getHttpMethodParameterOverride() +{ +return self::$httpMethodParameterOverride; +} +public function get($key, $default = null, $deep = false) +{ +if ($deep) { +@trigger_error('Using paths to find deeper items in '.__METHOD__.' is deprecated since version 2.8 and will be removed in 3.0. Filter the returned value in your own code instead.', E_USER_DEPRECATED); +} +if ($this !== $result = $this->query->get($key, $this, $deep)) { +return $result; +} +if ($this !== $result = $this->attributes->get($key, $this, $deep)) { +return $result; +} +if ($this !== $result = $this->request->get($key, $this, $deep)) { +return $result; +} +return $default; +} +public function getSession() +{ +return $this->session; +} +public function hasPreviousSession() +{ +return $this->hasSession() && $this->cookies->has($this->session->getName()); +} +public function hasSession() +{ +return null !== $this->session; +} +public function setSession(SessionInterface $session) +{ +$this->session = $session; +} +public function getClientIps() +{ +$ip = $this->server->get('REMOTE_ADDR'); +if (!$this->isFromTrustedProxy()) { +return array($ip); +} +return $this->getTrustedValues(self::HEADER_CLIENT_IP, $ip) ?: array($ip); +} +public function getClientIp() +{ +$ipAddresses = $this->getClientIps(); +return $ipAddresses[0]; +} +public function getScriptName() +{ +return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME','')); +} +public function getPathInfo() +{ +if (null === $this->pathInfo) { +$this->pathInfo = $this->preparePathInfo(); +} +return $this->pathInfo; +} +public function getBasePath() +{ +if (null === $this->basePath) { +$this->basePath = $this->prepareBasePath(); +} +return $this->basePath; +} +public function getBaseUrl() +{ +if (null === $this->baseUrl) { +$this->baseUrl = $this->prepareBaseUrl(); +} +return $this->baseUrl; +} +public function getScheme() +{ +return $this->isSecure() ?'https':'http'; +} +public function getPort() +{ +if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_PORT)) { +$host = $host[0]; +} elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) { +$host = $host[0]; +} elseif (!$host = $this->headers->get('HOST')) { +return $this->server->get('SERVER_PORT'); +} +if ($host[0] ==='[') { +$pos = strpos($host,':', strrpos($host,']')); +} else { +$pos = strrpos($host,':'); +} +if (false !== $pos) { +return (int) substr($host, $pos + 1); +} +return'https'=== $this->getScheme() ? 443 : 80; +} +public function getUser() +{ +return $this->headers->get('PHP_AUTH_USER'); +} +public function getPassword() +{ +return $this->headers->get('PHP_AUTH_PW'); +} +public function getUserInfo() +{ +$userinfo = $this->getUser(); +$pass = $this->getPassword(); +if (''!= $pass) { +$userinfo .= ":$pass"; +} +return $userinfo; +} +public function getHttpHost() +{ +$scheme = $this->getScheme(); +$port = $this->getPort(); +if (('http'== $scheme && $port == 80) || ('https'== $scheme && $port == 443)) { +return $this->getHost(); +} +return $this->getHost().':'.$port; +} +public function getRequestUri() +{ +if (null === $this->requestUri) { +$this->requestUri = $this->prepareRequestUri(); +} +return $this->requestUri; +} +public function getSchemeAndHttpHost() +{ +return $this->getScheme().'://'.$this->getHttpHost(); +} +public function getUri() +{ +if (null !== $qs = $this->getQueryString()) { +$qs ='?'.$qs; +} +return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs; +} +public function getUriForPath($path) +{ +return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; +} +public function getRelativeUriForPath($path) +{ +if (!isset($path[0]) ||'/'!== $path[0]) { +return $path; +} +if ($path === $basePath = $this->getPathInfo()) { +return''; +} +$sourceDirs = explode('/', isset($basePath[0]) &&'/'=== $basePath[0] ? substr($basePath, 1) : $basePath); +$targetDirs = explode('/', isset($path[0]) &&'/'=== $path[0] ? substr($path, 1) : $path); +array_pop($sourceDirs); +$targetFile = array_pop($targetDirs); +foreach ($sourceDirs as $i => $dir) { +if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { +unset($sourceDirs[$i], $targetDirs[$i]); +} else { +break; +} +} +$targetDirs[] = $targetFile; +$path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs); +return !isset($path[0]) ||'/'=== $path[0] +|| false !== ($colonPos = strpos($path,':')) && ($colonPos < ($slashPos = strpos($path,'/')) || false === $slashPos) +? "./$path" : $path; +} +public function getQueryString() +{ +$qs = static::normalizeQueryString($this->server->get('QUERY_STRING')); +return''=== $qs ? null : $qs; +} +public function isSecure() +{ +if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_CLIENT_PROTO)) { +return in_array(strtolower($proto[0]), array('https','on','ssl','1'), true); +} +$https = $this->server->get('HTTPS'); +return !empty($https) &&'off'!== strtolower($https); +} +public function getHost() +{ +if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) { +$host = $host[0]; +} elseif (!$host = $this->headers->get('HOST')) { +if (!$host = $this->server->get('SERVER_NAME')) { +$host = $this->server->get('SERVER_ADDR',''); +} +} +$host = strtolower(preg_replace('/:\d+$/','', trim($host))); +if ($host &&''!== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/','', $host)) { +throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host)); +} +if (count(self::$trustedHostPatterns) > 0) { +if (in_array($host, self::$trustedHosts)) { +return $host; +} +foreach (self::$trustedHostPatterns as $pattern) { +if (preg_match($pattern, $host)) { +self::$trustedHosts[] = $host; +return $host; +} +} +throw new \UnexpectedValueException(sprintf('Untrusted Host "%s"', $host)); +} +return $host; +} +public function setMethod($method) +{ +$this->method = null; +$this->server->set('REQUEST_METHOD', $method); +} +public function getMethod() +{ +if (null === $this->method) { +$this->method = strtoupper($this->server->get('REQUEST_METHOD','GET')); +if ('POST'=== $this->method) { +if ($method = $this->headers->get('X-HTTP-METHOD-OVERRIDE')) { +$this->method = strtoupper($method); +} elseif (self::$httpMethodParameterOverride) { +$this->method = strtoupper($this->request->get('_method', $this->query->get('_method','POST'))); +} +} +} +return $this->method; +} +public function getRealMethod() +{ +return strtoupper($this->server->get('REQUEST_METHOD','GET')); +} +public function getMimeType($format) +{ +if (null === static::$formats) { +static::initializeFormats(); +} +return isset(static::$formats[$format]) ? static::$formats[$format][0] : null; +} +public function getFormat($mimeType) +{ +$canonicalMimeType = null; +if (false !== $pos = strpos($mimeType,';')) { +$canonicalMimeType = substr($mimeType, 0, $pos); +} +if (null === static::$formats) { +static::initializeFormats(); +} +foreach (static::$formats as $format => $mimeTypes) { +if (in_array($mimeType, (array) $mimeTypes)) { +return $format; +} +if (null !== $canonicalMimeType && in_array($canonicalMimeType, (array) $mimeTypes)) { +return $format; +} +} +} +public function setFormat($format, $mimeTypes) +{ +if (null === static::$formats) { +static::initializeFormats(); +} +static::$formats[$format] = is_array($mimeTypes) ? $mimeTypes : array($mimeTypes); +} +public function getRequestFormat($default ='html') +{ +if (null === $this->format) { +$this->format = $this->get('_format'); +} +return null === $this->format ? $default : $this->format; +} +public function setRequestFormat($format) +{ +$this->format = $format; +} +public function getContentType() +{ +return $this->getFormat($this->headers->get('CONTENT_TYPE')); +} +public function setDefaultLocale($locale) +{ +$this->defaultLocale = $locale; +if (null === $this->locale) { +$this->setPhpDefaultLocale($locale); +} +} +public function getDefaultLocale() +{ +return $this->defaultLocale; +} +public function setLocale($locale) +{ +$this->setPhpDefaultLocale($this->locale = $locale); +} +public function getLocale() +{ +return null === $this->locale ? $this->defaultLocale : $this->locale; +} +public function isMethod($method) +{ +return $this->getMethod() === strtoupper($method); +} +public function isMethodSafe() +{ +return in_array($this->getMethod(), 0 < func_num_args() && !func_get_arg(0) ? array('GET','HEAD','OPTIONS','TRACE') : array('GET','HEAD')); +} +public function isMethodCacheable() +{ +return in_array($this->getMethod(), array('GET','HEAD')); +} +public function getContent($asResource = false) +{ +$currentContentIsResource = is_resource($this->content); +if (\PHP_VERSION_ID < 50600 && false === $this->content) { +throw new \LogicException('getContent() can only be called once when using the resource return type and PHP below 5.6.'); +} +if (true === $asResource) { +if ($currentContentIsResource) { +rewind($this->content); +return $this->content; +} +if (is_string($this->content)) { +$resource = fopen('php://temp','r+'); +fwrite($resource, $this->content); +rewind($resource); +return $resource; +} +$this->content = false; +return fopen('php://input','rb'); +} +if ($currentContentIsResource) { +rewind($this->content); +return stream_get_contents($this->content); +} +if (null === $this->content || false === $this->content) { +$this->content = file_get_contents('php://input'); +} +return $this->content; +} +public function getETags() +{ +return preg_split('/\s*,\s*/', $this->headers->get('if_none_match'), null, PREG_SPLIT_NO_EMPTY); +} +public function isNoCache() +{ +return $this->headers->hasCacheControlDirective('no-cache') ||'no-cache'== $this->headers->get('Pragma'); +} +public function getPreferredLanguage(array $locales = null) +{ +$preferredLanguages = $this->getLanguages(); +if (empty($locales)) { +return isset($preferredLanguages[0]) ? $preferredLanguages[0] : null; +} +if (!$preferredLanguages) { +return $locales[0]; +} +$extendedPreferredLanguages = array(); +foreach ($preferredLanguages as $language) { +$extendedPreferredLanguages[] = $language; +if (false !== $position = strpos($language,'_')) { +$superLanguage = substr($language, 0, $position); +if (!in_array($superLanguage, $preferredLanguages)) { +$extendedPreferredLanguages[] = $superLanguage; +} +} +} +$preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales)); +return isset($preferredLanguages[0]) ? $preferredLanguages[0] : $locales[0]; +} +public function getLanguages() +{ +if (null !== $this->languages) { +return $this->languages; +} +$languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); +$this->languages = array(); +foreach ($languages as $lang => $acceptHeaderItem) { +if (false !== strpos($lang,'-')) { +$codes = explode('-', $lang); +if ('i'=== $codes[0]) { +if (count($codes) > 1) { +$lang = $codes[1]; +} +} else { +for ($i = 0, $max = count($codes); $i < $max; ++$i) { +if ($i === 0) { +$lang = strtolower($codes[0]); +} else { +$lang .='_'.strtoupper($codes[$i]); +} +} +} +} +$this->languages[] = $lang; +} +return $this->languages; +} +public function getCharsets() +{ +if (null !== $this->charsets) { +return $this->charsets; +} +return $this->charsets = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all()); +} +public function getEncodings() +{ +if (null !== $this->encodings) { +return $this->encodings; +} +return $this->encodings = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all()); +} +public function getAcceptableContentTypes() +{ +if (null !== $this->acceptableContentTypes) { +return $this->acceptableContentTypes; +} +return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all()); +} +public function isXmlHttpRequest() +{ +return'XMLHttpRequest'== $this->headers->get('X-Requested-With'); +} +protected function prepareRequestUri() +{ +$requestUri =''; +if ($this->headers->has('X_ORIGINAL_URL')) { +$requestUri = $this->headers->get('X_ORIGINAL_URL'); +$this->headers->remove('X_ORIGINAL_URL'); +$this->server->remove('HTTP_X_ORIGINAL_URL'); +$this->server->remove('UNENCODED_URL'); +$this->server->remove('IIS_WasUrlRewritten'); +} elseif ($this->headers->has('X_REWRITE_URL')) { +$requestUri = $this->headers->get('X_REWRITE_URL'); +$this->headers->remove('X_REWRITE_URL'); +} elseif ($this->server->get('IIS_WasUrlRewritten') =='1'&& $this->server->get('UNENCODED_URL') !='') { +$requestUri = $this->server->get('UNENCODED_URL'); +$this->server->remove('UNENCODED_URL'); +$this->server->remove('IIS_WasUrlRewritten'); +} elseif ($this->server->has('REQUEST_URI')) { +$requestUri = $this->server->get('REQUEST_URI'); +$schemeAndHttpHost = $this->getSchemeAndHttpHost(); +if (strpos($requestUri, $schemeAndHttpHost) === 0) { +$requestUri = substr($requestUri, strlen($schemeAndHttpHost)); +} +} elseif ($this->server->has('ORIG_PATH_INFO')) { +$requestUri = $this->server->get('ORIG_PATH_INFO'); +if (''!= $this->server->get('QUERY_STRING')) { +$requestUri .='?'.$this->server->get('QUERY_STRING'); +} +$this->server->remove('ORIG_PATH_INFO'); +} +$this->server->set('REQUEST_URI', $requestUri); +return $requestUri; +} +protected function prepareBaseUrl() +{ +$filename = basename($this->server->get('SCRIPT_FILENAME')); +if (basename($this->server->get('SCRIPT_NAME')) === $filename) { +$baseUrl = $this->server->get('SCRIPT_NAME'); +} elseif (basename($this->server->get('PHP_SELF')) === $filename) { +$baseUrl = $this->server->get('PHP_SELF'); +} elseif (basename($this->server->get('ORIG_SCRIPT_NAME')) === $filename) { +$baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); } else { +$path = $this->server->get('PHP_SELF',''); +$file = $this->server->get('SCRIPT_FILENAME',''); +$segs = explode('/', trim($file,'/')); +$segs = array_reverse($segs); +$index = 0; +$last = count($segs); +$baseUrl =''; +do { +$seg = $segs[$index]; +$baseUrl ='/'.$seg.$baseUrl; +++$index; +} while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos); +} +$requestUri = $this->getRequestUri(); +if ($requestUri !==''&& $requestUri[0] !=='/') { +$requestUri ='/'.$requestUri; +} +if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { +return $prefix; +} +if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(dirname($baseUrl),'/'.DIRECTORY_SEPARATOR).'/')) { +return rtrim($prefix,'/'.DIRECTORY_SEPARATOR); +} +$truncatedRequestUri = $requestUri; +if (false !== $pos = strpos($requestUri,'?')) { +$truncatedRequestUri = substr($requestUri, 0, $pos); +} +$basename = basename($baseUrl); +if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) { +return''; +} +if (strlen($requestUri) >= strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && $pos !== 0) { +$baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl)); +} +return rtrim($baseUrl,'/'.DIRECTORY_SEPARATOR); +} +protected function prepareBasePath() +{ +$filename = basename($this->server->get('SCRIPT_FILENAME')); +$baseUrl = $this->getBaseUrl(); +if (empty($baseUrl)) { +return''; +} +if (basename($baseUrl) === $filename) { +$basePath = dirname($baseUrl); +} else { +$basePath = $baseUrl; +} +if ('\\'=== DIRECTORY_SEPARATOR) { +$basePath = str_replace('\\','/', $basePath); +} +return rtrim($basePath,'/'); +} +protected function preparePathInfo() +{ +$baseUrl = $this->getBaseUrl(); +if (null === ($requestUri = $this->getRequestUri())) { +return'/'; +} +if (false !== $pos = strpos($requestUri,'?')) { +$requestUri = substr($requestUri, 0, $pos); +} +if ($requestUri !==''&& $requestUri[0] !=='/') { +$requestUri ='/'.$requestUri; +} +$pathInfo = substr($requestUri, strlen($baseUrl)); +if (null !== $baseUrl && (false === $pathInfo ||''=== $pathInfo)) { +return'/'; +} elseif (null === $baseUrl) { +return $requestUri; +} +return (string) $pathInfo; +} +protected static function initializeFormats() +{ +static::$formats = array('html'=> array('text/html','application/xhtml+xml'),'txt'=> array('text/plain'),'js'=> array('application/javascript','application/x-javascript','text/javascript'),'css'=> array('text/css'),'json'=> array('application/json','application/x-json'),'xml'=> array('text/xml','application/xml','application/x-xml'),'rdf'=> array('application/rdf+xml'),'atom'=> array('application/atom+xml'),'rss'=> array('application/rss+xml'),'form'=> array('application/x-www-form-urlencoded'), +); +} +private function setPhpDefaultLocale($locale) +{ +try { +if (class_exists('Locale', false)) { +\Locale::setDefault($locale); +} +} catch (\Exception $e) { +} +} +private function getUrlencodedPrefix($string, $prefix) +{ +if (0 !== strpos(rawurldecode($string), $prefix)) { +return false; +} +$len = strlen($prefix); +if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { +return $match[0]; +} +return false; +} +private static function createRequestFromFactory(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) +{ +if (self::$requestFactory) { +$request = call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content); +if (!$request instanceof self) { +throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); +} +return $request; +} +return new static($query, $request, $attributes, $cookies, $files, $server, $content); +} +private function isFromTrustedProxy() +{ +return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies); +} +private function getTrustedValues($type, $ip = null) +{ +$clientValues = array(); +$forwardedValues = array(); +if (self::$trustedHeaders[$type] && $this->headers->has(self::$trustedHeaders[$type])) { +foreach (explode(',', $this->headers->get(self::$trustedHeaders[$type])) as $v) { +$clientValues[] = (self::HEADER_CLIENT_PORT === $type ?'0.0.0.0:':'').trim($v); +} +} +if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) { +$forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); +$forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array(); +} +if (null !== $ip) { +$clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); +$forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); +} +if ($forwardedValues === $clientValues || !$clientValues) { +return $forwardedValues; +} +if (!$forwardedValues) { +return $clientValues; +} +if (!$this->isForwardedValid) { +return null !== $ip ? array('0.0.0.0', $ip) : array(); +} +$this->isForwardedValid = false; +throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::$trustedHeaders[self::HEADER_FORWARDED], self::$trustedHeaders[$type])); +} +private function normalizeAndFilterClientIps(array $clientIps, $ip) +{ +if (!$clientIps) { +return array(); +} +$clientIps[] = $ip; $firstTrustedIp = null; +foreach ($clientIps as $key => $clientIp) { +if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) { +$clientIps[$key] = $clientIp = $match[1]; +} +if (!filter_var($clientIp, FILTER_VALIDATE_IP)) { +unset($clientIps[$key]); +continue; +} +if (IpUtils::checkIp($clientIp, self::$trustedProxies)) { +unset($clientIps[$key]); +if (null === $firstTrustedIp) { +$firstTrustedIp = $clientIp; +} +} +} +return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp); +} +} +} diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config.yml new file mode 100644 index 000000000..6a2be1ca6 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config.yml @@ -0,0 +1,73 @@ +imports: + - { resource: parameters.yml } + +framework: + secret: Not very secret + router: { resource: "%kernel.root_dir%/config/routing.yml" } + form: true + csrf_protection: true + session: ~ + default_locale: fr + translator: { fallback: fr } + profiler: { only_exceptions: false } + templating: #required for assetic. Remove if not needed + engines: ['twig'] + +doctrine: + dbal: + driver: pdo_pgsql + host: "%database_host%" + port: "%database_port%" + dbname: "%database_name%" + user: "%database_user%" + password: "%database_password%" + charset: UTF8 + orm: + auto_generate_proxy_classes: "%kernel.debug%" + auto_mapping: true + +# Assetic Configuration +assetic: + debug: "%kernel.debug%" + use_controller: false + bundles: [ ] + #java: /usr/bin/java + filters: + cssrewrite: ~ + +chill_main: + available_languages: [ fr, nl, en ] + +security: + providers: + users: + entity: + class: Chill\MainBundle\Entity\User + property: username + + encoders: + Chill\MainBundle\Entity\User: + algorithm: bcrypt + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + + + + default: + anonymous: ~ + http_basic: ~ + form_login: + csrf_parameter: _csrf_token + csrf_token_id: authenticate + csrf_provider: form.csrf_provider + + logout: ~ + + + access_control: + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/, roles: ROLE_USER } diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_dev.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_dev.yml new file mode 100644 index 000000000..81e0f80f4 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_dev.yml @@ -0,0 +1,7 @@ +imports: + - { resource: config.yml } #here we import a config.yml file, this is not required + +framework: + test: ~ + session: + storage_id: session.storage.filesystem \ No newline at end of file diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_test.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_test.yml new file mode 100644 index 000000000..812b1a09c --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/config_test.yml @@ -0,0 +1,7 @@ +imports: + - { resource: config.yml } #here we import a config.yml file, this is not required + +framework: + test: ~ + session: + storage_id: session.storage.filesystem diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml new file mode 100644 index 000000000..9e3b75daf --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml @@ -0,0 +1,7 @@ +parameters: + database_host: chill__database + database_port: 5432 + database_name: postgres + database_user: postgres + database_password: postgres + locale: fr diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml new file mode 100644 index 000000000..f0e6be25e --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml @@ -0,0 +1,13 @@ +parameters: + database_host: localhost +# database_host: 127.0.0.1 + database_port: 5435 +# database_port: 5454 + database_name: postgres + database_user: postgres + database_password: postgres + locale: fr + secret: ThisTokenIsNotSoSecretChangeIt + debug_toolbar: true + debug_redirects: false + use_assetic_controller: true diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml.dist b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml.dist new file mode 100644 index 000000000..65d50299f --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/parameters.yml.dist @@ -0,0 +1,7 @@ +parameters: + database_host: 127.0.0.1 + database_port: 5432 + database_name: symfony + database_user: symfony + database_password: symfony + locale: fr \ No newline at end of file diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/routing.yml b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/routing.yml new file mode 100644 index 000000000..b91644d17 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/config/routing.yml @@ -0,0 +1,3 @@ +chill_routes: + resource: . + type: chill_routes \ No newline at end of file diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/console b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/console new file mode 100644 index 000000000..f6fd8e5ef --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/app/console @@ -0,0 +1,27 @@ +#!/usr/bin/env php +getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); +$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; + +if ($debug) { + Debug::enable(); +} + +$kernel = new AppKernel($env, $debug); +$application = new Application($kernel); +$application->run($input); diff --git a/src/Bundle/ChillReport/Resources/test/Fixtures/App/web/.gitignore b/src/Bundle/ChillReport/Resources/test/Fixtures/App/web/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/src/Bundle/ChillReport/Resources/test/Fixtures/App/web/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/Bundle/ChillReport/Resources/views/Report/edit.html.twig b/src/Bundle/ChillReport/Resources/views/Report/edit.html.twig new file mode 100644 index 000000000..9ae03e174 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/edit.html.twig @@ -0,0 +1,47 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'report_select_type' %} + +{% block title %}{{ 'Report edit' |trans }}{% endblock title %} + +{% block personcontent %} + {{ form_start(edit_form) }} + + {{ form_row(edit_form.user) }} + {{ form_row(edit_form.date) }} + {{ form_row(edit_form.scope) }} + {{ form_row(edit_form.cFData) }} + + {{ form_widget(edit_form) }} + + {{ form_end(edit_form) }} +{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/export.csv.twig b/src/Bundle/ChillReport/Resources/views/Report/export.csv.twig new file mode 100644 index 000000000..b0482afe9 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/export.csv.twig @@ -0,0 +1,29 @@ +"{{ 'Report id'|trans }}",{# +#}"{{ 'Person'|trans }}",{# +#}"{{ 'Person id'|trans }}",{# +#}"{{ 'Date'|trans }}",{# +#}"{{ 'User'|trans }}",{# +#}"{{ 'Report type'|trans }}",{# +#}{% for customField in cf_group.activeCustomFields %}{# + #}"{{ chill_custom_field_label(customField) }}"{% if not loop.last %},{% endif %}{# +#}{% endfor %}{# + +#}{{ '\r\n'|raw }}{# +#}{% for report in reports %}{# + #}{{ report.id }},{# + #}"{{ report.person|csv_cell }}",{# + #}"{{ report.person.id|csv_cell }}",{# + #}"{{ report.date|format_date('short', 'none') }}",{# + #}"{{ report.user|csv_cell }}",{# + #}"{{ report.cFGroup.getName(app.request.locale)|csv_cell }}",{# + #}{% for customField in report.cFGroup.activeCustomFields %}{# + #}{% if customField.type == 'title' %}{# + #}""{# + #}{% else %}{# + #}"{{ chill_custom_field_widget(report.cFData , customField, 'csv') }}"{# + #}{% endif %}{# + #}{% if not loop.last %},{% endif %}{# + #}{% endfor %}{# + + #}{{ '\r\n'|raw }}{# +#}{% endfor %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/list.html.twig b/src/Bundle/ChillReport/Resources/views/Report/list.html.twig new file mode 100644 index 000000000..485771f51 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/list.html.twig @@ -0,0 +1,80 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'report_select_type' %} + +{% block title %}{{ 'Report list' |trans }}{% endblock title %} + +{% block personcontent %} + +

{{ 'Report list'|trans }}

+ + {% if reports|length == 0 %} +

+ {{ "No report registered for this person."|trans }} +

+ {% else %} + + + + + + + + + + + {% for report in reports %} + + + + + + + {% endfor %} + +
{{ 'Date' | trans }}{{ 'Report type' | trans }}{{ 'Circle' | trans }} 
{% if report.date %}{{ report.date|format_date('long') }}{% endif %}{{ report.cFGroup.getName|localize_translatable_string }}{{ report.scope.name|localize_translatable_string }} +
    + {% if is_granted('CHILL_REPORT_SEE', report) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_REPORT_UPDATE', report) %} +
  • + +
  • + {% endif %} +
+
+ + {% if reports|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} + + {% endif %} + + + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/new.html.twig b/src/Bundle/ChillReport/Resources/views/Report/new.html.twig new file mode 100644 index 000000000..1fc18a735 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/new.html.twig @@ -0,0 +1,45 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'report_select_type' %} + +{% block title %}{{ 'Add a report'|trans }}{% endblock title %} + +{% block personcontent %} + {{ form_start(form) }} + + {{ form_row(form.user) }} + {{ form_row(form.date) }} + {{ form_row(form.scope) }} + {{ form_row(form.cFData) }} + + {{ form_end(form) }} +{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/select_report_type.html.twig b/src/Bundle/ChillReport/Resources/views/Report/select_report_type.html.twig new file mode 100644 index 000000000..67983a7f8 --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/select_report_type.html.twig @@ -0,0 +1,40 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'report_select_type' %} + +{% block title %}{{ 'Add a report'|trans() }}{% endblock title %} + +{% block personcontent %} + +{{ form_start(form) }} + {{ form_widget(form.cFGroup) }} + + +{{ form_end(form) }} + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/select_report_type_for_export.html.twig b/src/Bundle/ChillReport/Resources/views/Report/select_report_type_for_export.html.twig new file mode 100644 index 000000000..56232cfbe --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/select_report_type_for_export.html.twig @@ -0,0 +1,33 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillMain/Export/layout.html.twig" %} + +{% set activeRouteKey = 'report_export_list' %} + +{% block title %}{{ 'Reports export'|trans() }}{% endblock title %} + +{% block export_content %} + +{{ form_start(form) }} + {{ form_widget(form.cFGroup) }} + +{{ form_end(form) }} + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Report/view.html.twig b/src/Bundle/ChillReport/Resources/views/Report/view.html.twig new file mode 100644 index 000000000..f1c6ffdae --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Report/view.html.twig @@ -0,0 +1,57 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'report_select_type' %} + +{% block title %}{{ 'Report view' |trans() }}{% endblock title %} + +{% block personcontent %} + +

{{ 'Report view : %name%' | trans({ '%name%' : entity.cFGroup.getName|localize_translatable_string } ) }}

+ +

{{ 'Details'|trans }}

+ +
+
{{ 'Person'|trans }} :
+
{{ entity.person }}
+
{{ 'Circle'|trans }} :
+
{{ entity.scope.name|localize_translatable_string }}
+
{{ 'Date'|trans }} :
+
{{ entity.date|format_date('long') }}
+
{{ 'User'|trans }} :
+
{{ entity.user }}
+ +
+ +

{{ 'Report data'|trans }}

+ +
+ {{ chill_custom_fields_group_widget(entity.cFData, entity.cFGroup) }} +
+ + +{% endblock %} diff --git a/src/Bundle/ChillReport/Resources/views/Search/results.html.twig b/src/Bundle/ChillReport/Resources/views/Search/results.html.twig new file mode 100644 index 000000000..68367bebd --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Search/results.html.twig @@ -0,0 +1,46 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +

{{ 'Reports search results'|trans }}

+ +

{{ '%total% reports matching the search "%pattern%"'|transchoice( total, {'%pattern%': pattern, '%total%' : total}) }}

+ +{% if reports|length > 0 %} + + + + + + + + + + + + + {% for report in reports %} + + + + + + + + + {% endfor %} + +
{{ 'Person'|trans }}{{ 'Date'|trans }}{{ 'Report type'|trans }}{{ 'Scope'|trans }}  
{{ report.person }}{% if report.date %}{{ report.date|format_date('long') }}{% endif %}{{ report.cFGroup.getName|localize_translatable_string }}{{ report.scope.name|localize_translatable_string }}{{ 'View the report' | trans }}{{ 'Update the report' | trans }}
+{% endif %} diff --git a/src/Bundle/ChillReport/Resources/views/Timeline/report_person_context.html.twig b/src/Bundle/ChillReport/Resources/views/Timeline/report_person_context.html.twig new file mode 100644 index 000000000..e9bcba85b --- /dev/null +++ b/src/Bundle/ChillReport/Resources/views/Timeline/report_person_context.html.twig @@ -0,0 +1,40 @@ +
+

{{ report.date|format_date('long') }} / {{ 'Report'|trans }}

+
+ {{ '%user% has filled a %report_label% report'|trans( + { + '%user%' : user, + '%report_label%': report.CFGroup.name|localize_translatable_string, + '%date%' : report.date|format_date('long') } + ) }} +
+ {% if custom_fields_in_summary|length > 0 %} +
+
+ {% for field in custom_fields_in_summary %} + {% if field.type == 'title' %} + {{ chill_custom_field_widget(report.cFData, field) }} + {% else %} +
{{ chill_custom_field_label(field) }}
+
{{ chill_custom_field_widget(report.cFData, field) }}
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + + +
diff --git a/src/Bundle/ChillReport/Search/ReportSearch.php b/src/Bundle/ChillReport/Search/ReportSearch.php new file mode 100644 index 000000000..be4063802 --- /dev/null +++ b/src/Bundle/ChillReport/Search/ReportSearch.php @@ -0,0 +1,186 @@ + + * + * 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\ReportBundle\Search; + +use Chill\MainBundle\Search\AbstractSearch; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Chill\MainBundle\Search\ParsingException; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Entity\Scope; + +/** + * Search amongst reports + * + * @author Julien Fastré + */ +class ReportSearch extends AbstractSearch implements ContainerAwareInterface +{ + use \Symfony\Component\DependencyInjection\ContainerAwareTrait; + + /** + * @var EntityManagerInterface + */ + private $em; + + /** + * + * @var AuthorizationHelper + */ + private $helper; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + private $user; + + public function __construct(EntityManagerInterface $em, + AuthorizationHelper $helper, TokenStorageInterface $tokenStorage) + { + $this->em = $em; + $this->helper = $helper; + + if(! $tokenStorage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User) { + throw new \RuntimeException('an user must be associated with token'); + } + $this->user = $tokenStorage->getToken()->getUser(); + } + + public function getOrder() + { + return 10000; + } + + public function isActiveByDefault() + { + return false; + } + + public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html') + { + return $this->container->get('templating')->render('ChillReportBundle:Search:results.html.twig', array( + 'reports' => $this->getReports($terms, $start, $limit), + 'total' => $this->count($terms), + 'pattern' => $this->recomposePattern($terms, array( 'date'), 'report') + )); + } + + private function getReports(array $terms, $start, $limit) + { + $qb = $this->buildQuery($terms); + + $qb->select('r') + ->setMaxResults($limit) + ->setFirstResult($start) + ->orderBy('r.date', 'desc') + ; + + $reportQuery = $qb->getQuery(); + $reportQuery->setFetchMode("Chill\ReportBundle\Entity\Report", "person", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER); + + return $reportQuery->getResult(); + } + + private function count(array $terms) + { + $qb = $this->buildQuery($terms); + + $qb->select('COUNT(r.id)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + + + /** + * @param array $terms the terms + * @return \Doctrine\ORM\QueryBuilder + */ + private function buildQuery(array $terms) + { + + $query = $this->em->createQueryBuilder(); + + $query->from('ChillReportBundle:Report', 'r'); + + //throw a parsing exception if key 'date' and default is set + if (array_key_exists('date', $terms) && $terms['_default'] !== '') { + throw new ParsingException('You may not set a date argument and a date in default'); + } + //throw a parsing exception if no argument except report + if (!array_key_exists('date', $terms) && $terms['_default'] === '') { + throw new ParsingException('You must provide either a date:YYYY-mm-dd argument or a YYYY-mm-dd default search'); + } + + + if (array_key_exists('date', $terms)) { + $query->andWhere($query->expr()->eq('r.date', ':date')) + ->setParameter('date', $this->parseDate($terms['date'])) + ; + } elseif (array_key_exists('_default', $terms)) { + $query->andWhere($query->expr()->eq('r.date', ':date')) + ->setParameter('date', $this->parseDate($terms['_default'])) + ; + } + + $query->andWhere($this->addACL($query)); + + return $query; + } + + private function addACL(QueryBuilder $qb) + { + //adding join + $qb->join('r.person', 'p'); + + $role = new Role('CHILL_REPORT_SEE'); + $reachableCenters = $this->helper->getReachableCenters($this->user, $role); + + $whereElement = $qb->expr()->orX(); + $i = 0; + foreach ($reachableCenters as $center) { + $reachableScopesId = array_map( + function (Scope $scope) { return $scope->getId(); }, + $this->helper->getReachableScopes($this->user, $role, $center) + ); + $whereElement->add( + $qb->expr()->andX( + $qb->expr()->eq('p.center', ':center_'.$i), + $qb->expr()->in('r.scope', ':reachable_scopes_'.$i) + ) + ) + ; + $qb->setParameter('center_'.$i, $center); + $qb->setParameter('reachable_scopes_'.$i, $reachableScopesId); + } + + return $whereElement; + } + + public function supports($domain, $format = 'html') + { + return $domain === 'report'; + } + +} diff --git a/src/Bundle/ChillReport/Security/Authorization/ReportVoter.php b/src/Bundle/ChillReport/Security/Authorization/ReportVoter.php new file mode 100644 index 000000000..5741b567b --- /dev/null +++ b/src/Bundle/ChillReport/Security/Authorization/ReportVoter.php @@ -0,0 +1,92 @@ + + * + * 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\ReportBundle\Security\Authorization; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +use Chill\MainBundle\Security\Authorization\AbstractChillVoter; +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; + + +/** + * + * + * @author Julien Fastré + */ +class ReportVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +{ + const CREATE = 'CHILL_REPORT_CREATE'; + const SEE = 'CHILL_REPORT_SEE'; + const UPDATE = 'CHILL_REPORT_UPDATE'; + const LISTS = 'CHILL_REPORT_LISTS'; + + /** + * + * @var AuthorizationHelper + */ + protected $helper; + + + public function __construct(AuthorizationHelper $helper) + { + $this->helper = $helper; + } + + + protected function supports($attribute, $subject) + { + if ($subject instanceof Report) { + return \in_array($attribute, [ + self::CREATE, self::UPDATE, self::SEE + ]); + } elseif ($subject instanceof Center) { + return $attribute === self::LISTS; + } + } + + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + if (!$token->getUser() instanceof User) { + return false; + } + return $this->helper->userHasAccess($token->getUser(), $subject, $attribute); + } + + + public function getRoles() + { + return [self::CREATE, self::UPDATE, self::SEE, self::LISTS]; + } + + public function getRolesWithoutScope() + { + return array(self::LISTS); + } + + public function getRolesWithHierarchy() + { + return [ 'Report' => $this->getRoles() ]; + } +} diff --git a/src/Bundle/ChillReport/Tests/Controller/ReportControllerNextTest.php b/src/Bundle/ChillReport/Tests/Controller/ReportControllerNextTest.php new file mode 100644 index 000000000..e3c96bf22 --- /dev/null +++ b/src/Bundle/ChillReport/Tests/Controller/ReportControllerNextTest.php @@ -0,0 +1,178 @@ + + * + * 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\ReportBundle\Tests\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Chill\PersonBundle\Entity\Person; +use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup; +use Symfony\Component\BrowserKit\Client; + +/** + * This class is much well writtend than ReportControllerTest class, and will + * replace ReportControllerTest in the future. + * + * @author Julien Fastré + */ +class ReportControllerNextTest extends WebTestCase +{ + /** + * + * @var Person + */ + protected $person; + + /** + * + * @var CustomFieldsGroup + */ + protected $group; + + + public function setUp() + { + static::bootKernel(); + // get person from fixture + $em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $this->person = $em + ->getRepository('ChillPersonBundle:Person') + ->findOneBy(array( + 'lastName' => 'Charline', + 'firstName' => 'Depardieu' + ) + ); + + if ($this->person === NULL) { + throw new \RuntimeException("The expected person is not present in the database. " + . "Did you run `php app/console doctrine:fixture:load` before launching tests ? " + . "(expecting person is 'Charline Depardieu'"); + } + + // get custom fields group from fixture + $customFieldsGroups = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->findBy(array('entity' => 'Chill\ReportBundle\Entity\Report')) + ; + //filter customFieldsGroup to get only "situation de logement" + $filteredCustomFieldsGroupHouse = array_filter($customFieldsGroups, + function(CustomFieldsGroup $group) { + return in_array("Situation de logement", $group->getName()); + }); + $this->group = $filteredCustomFieldsGroupHouse[0]; + } + + public function testValidCreate() + { + $client = $this->getAuthenticatedClient(); + $form = $this->getReportForm($this->person, $this->group, $client); + + $form->get('chill_reportbundle_report[date]')->setValue( + (new \DateTime())->format('d-m-Y')); + + $client->submit($form); + + $this->assertTrue($client->getResponse()->isRedirect(), + "The next page is a redirection to the new report's view page"); + + } + + public function testUngrantedUserIsDeniedAccessOnListReports() + { + $client = $this->getAuthenticatedClient('center b_social'); + $client->request('GET', sprintf('/fr/person/%d/report/list', + $this->person->getId())); + + $this->assertEquals(403, $client->getResponse()->getStatusCode(), + 'assert that user for center b has a 403 status code when listing' + . 'reports on person from center a'); + } + + public function testUngrantedUserIsDeniedAccessOnReport() + { + $client = $this->getAuthenticatedClient('center b_social'); + $reports = static::$kernel->getContainer()->get('doctrine.orm.entity_manager') + ->getRepository('ChillReportBundle:Report') + ->findBy(array('person' => $this->person)); + $report = $reports[0]; + + $client->request('GET', sprintf('/fr/person/%d/report/%d/view', + $this->person->getId(), $report->getId())); + + $this->assertEquals(403, $client->getResponse()->getStatusCode(), + 'assert that user for center b has a 403 status code when ' + . 'trying to watch a report from person from center a'); + } + + public function testUngrantedUserIsDeniedReportNew() + { + $client = $this->getAuthenticatedClient('center b_social'); + + $client->request('GET', sprintf('fr/person/%d/report/cfgroup/%d/new', + $this->person->getId(), $this->group->getId())); + + $this->assertEquals(403, $client->getResponse()->getStatusCode(), + 'assert that user is denied on trying to show a form "new" for' + . ' a person on another center'); + } + + public function testUngrantedUserIsDeniedReportCreate() + { + $clientCenterA = $this->getAuthenticatedClient('center a_social'); + + $form = $this->getReportForm($this->person, $this->group, $clientCenterA); + + $clientCenterB = $this->getAuthenticatedClient('center b_social'); + $clientCenterB->submit($form); + + $this->assertEquals(403, $clientCenterB->getResponse()->getStatusCode(), + 'assert that user is denied on trying to show a form "new" for' + . ' a person on another center'); + } + + protected function getAuthenticatedClient($username = 'center a_social') + { + return static::createClient(array(), array( + 'PHP_AUTH_USER' => $username, + 'PHP_AUTH_PW' => 'password', + )); + } + + /** + * + * @param Person $person + * @param CustomFieldsGroup $group + * @param Client $client + * @return \Symfony\Component\DomCrawler\Form + */ + protected function getReportForm(Person $person, CustomFieldsGroup $group, Client $client) + { + $url = sprintf('fr/person/%d/report/cfgroup/%d/new', $person->getId(), + $group->getId()); + $crawler = $client->request('GET', $url); + + return $crawler->selectButton('Ajouter le rapport') + ->form(); + } + + + +} diff --git a/src/Bundle/ChillReport/Tests/Controller/ReportControllerTest.php b/src/Bundle/ChillReport/Tests/Controller/ReportControllerTest.php new file mode 100644 index 000000000..d7bf25f06 --- /dev/null +++ b/src/Bundle/ChillReport/Tests/Controller/ReportControllerTest.php @@ -0,0 +1,443 @@ +, + * + * 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\ReportBundle\Tests\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\DomCrawler\Form; +use Symfony\Component\DomCrawler\Link; +use Symfony\Component\DomCrawler\Crawler; +use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup; +use Chill\PersonBundle\Entity\Person; + +/** + * Test the life cycles of controllers, according to + * https://redmine.champs-libres.coop/projects/report/wiki/Test_plan_for_report_lifecycle + * + * @author Julien Fastré + */ +class ReportControllerTest extends WebTestCase +{ + + const REPORT_NAME_FIELD = 'cFGroup'; + + /** + * + * @var \Chill\PersonBundle\Entity\Person + */ + private static $person; + + /** + * + * @var \SClientymfony\Component\BrowserKit\ + */ + private static $client; + + private static $user; + + /** + * + * @var CustomFieldsGroup + */ + private static $group; + + /** + * + * @var \Doctrine\ORM\EntityManagerInterface + */ + private static $em; + + public static function setUpBeforeClass() + { + static::bootKernel(); + + static::$em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + //get a random person + static::$person = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('ChillPersonBundle:Person') + ->findOneBy(array( + 'lastName' => 'Charline', + 'firstName' => 'Depardieu' + ) + ); + + if (static::$person === NULL) { + throw new \RuntimeException("The expected person is not present in the database. " + . "Did you run `php app/console doctrine:fixture:load` before launching tests ? " + . "(expecting person is 'Charline Depardieu'"); + } + + $customFieldsGroups = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->findBy(array('entity' => 'Chill\ReportBundle\Entity\Report')) + ; + //filter customFieldsGroup to get only "situation de logement" + $filteredCustomFieldsGroupHouse = array_filter($customFieldsGroups, + function(CustomFieldsGroup $group) { + return in_array("Situation de logement", $group->getName()); + }); + static::$group = $filteredCustomFieldsGroupHouse[0]; + + static::$user = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:User') + ->findOneBy(array('username' => "center a_social")); + } + + public function setUp() + { + static::$client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_PW' => 'password', + )); + } + + /** + * + * @param type $username + * @return Client + */ + public function getAuthenticatedClient($username = 'center a_social') + { + return static::createClient(array(), array( + 'PHP_AUTH_USER' => $username, + 'PHP_AUTH_PW' => 'password', + )); + } + + /** + * Set up the browser to be at a random person general page (/fr/person/%d/general), + * check if there is a menu link for adding a new report and return this link (as producer) + * + * We assume that : + * - we are on a "person" page + * - there are more than one report model + * + */ + public function testMenu() + { + $client = $this->getAuthenticatedClient(); + $crawlerPersonPage = $client->request('GET', sprintf('/fr/person/%d/general', + static::$person->getId())); + + if (! $client->getResponse()->isSuccessful()) { + var_dump($crawlerPersonPage->html()); + throw new \RuntimeException('the request at person page failed'); + } + + $link = $crawlerPersonPage->selectLink("AJOUT D'UN RAPPORT")->link(); + $this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $link, + "There is a \"add a report\" link in menu"); + $this->assertContains(sprintf("/fr/person/%d/report/select/type/for/creation", + static::$person->getId()), $link->getUri(), + "There is a \"add a report\" link in menu"); + + return $link; + } + + /** + * + * @param \Symfony\Component\DomCrawler\Link $link + * @return type + * @depends testMenu + */ + public function testChooseReportModelPage(Link $link) + { + // When I click on "add a report" link in menu + $client = $this->getAuthenticatedClient(); + $crawlerAddAReportPage = $client->click($link); + + $form = $crawlerAddAReportPage->selectButton("Créer un nouveau rapport")->form(); + + $this->assertInstanceOf('Symfony\Component\DomCrawler\Form', $form, + 'I can see a form with a button "add a new report" '); + + $this->assertGreaterThan(1, count($form->get(self::REPORT_NAME_FIELD) + ->availableOptionValues()), + "I can choose between report models"); + + $possibleOptionsValue = $form->get(self::REPORT_NAME_FIELD) + ->availableOptionValues(); + $form->get(self::REPORT_NAME_FIELD)->setValue( + $possibleOptionsValue[array_rand($possibleOptionsValue)]); + + $client->submit($form); + + $this->assertTrue($client->getResponse()->isRedirect()); + return $client->followRedirect(); + } + + /** + * + * @param \Symfony\Component\DomCrawler\Crawler $crawlerNewReportPage + * @return type + * @depends testChooseReportModelPage + */ + public function testNewReportPage(Crawler $crawlerNewReportPage) + { + + $addForm = $crawlerNewReportPage + ->selectButton('Ajouter le rapport') + ->form(); + + $this->assertInstanceOf('Symfony\Component\DomCrawler\Form', $addForm, + 'I have a report form'); + + $this->isFormAsExpected($addForm); + + return $addForm; + } + + /** + * get a form for report new + * + * @param \Chill\ReportBundle\Tests\Controller\Person $person + * @param CustomFieldsGroup $group + * @param \Symfony\Component\BrowserKit\Client $client + * @return Form + */ + protected function getReportForm(Person $person, CustomFieldsGroup $group, + \Symfony\Component\BrowserKit\Client $client) + { + $url = sprintf('fr/person/%d/report/cfgroup/%d/new', $person->getId(), + $group->getId()); + $crawler = $client->request('GET', $url); + + return $crawler->selectButton('Ajouter le rapport') + ->form(); + } + + /** + * Test the expected field are present + * + * @param Form $form + * @param boolean $isDefault if the form should be at default values + * + */ + private function isFormAsExpected(Form $form, $isDefault = true) + { + $this->assertTrue($form->has('chill_reportbundle_report[date]'), + 'the report form have a field "date"' ); + $this->assertTrue($form->has('chill_reportbundle_report[user]'), + 'the report form have field "user" '); + + $this->assertEquals('select', $form->get('chill_reportbundle_report[user]') + ->getType(), "the user field is a select input"); + + if ($isDefault) { + $date = new \DateTime('now'); + $this->assertEquals( + $date->format('d-m-Y'), + $form->get('chill_reportbundle_report[date]')->getValue(), + "the date field contains the current date by default" + ); + + //resolve the user + $userId = $form->get('chill_reportbundle_report[user]')->getValue(); + + $this->assertEquals('center a_social', static::$user->getUsername(), + "the user field is the current user by default"); + } + } + + /** + * fill the form with correct data + * + * @param Form $form + */ + private function fillCorrectForm(Form $form) + { + $form->get('chill_reportbundle_report[date]')->setValue( + (new \DateTime())->format('d-m-Y')); + + return $form; + } + + /** + * Test that setting a Null date redirect to an error page + * + */ + public function testNullDate() + { + $client = $this->getAuthenticatedClient(); + $form = $this->getReportForm(static::$person, static::$group, + $client); + //var_dump($form); + $filledForm = $this->fillCorrectForm($form); + $filledForm->get('chill_reportbundle_report[date]')->setValue(''); + //$this->markTestSkipped(); + $crawler = $this->getAuthenticatedClient('center a_administrative')->submit($filledForm); + + $this->assertFalse($client->getResponse()->isRedirect()); + $this->assertGreaterThan(0, $crawler->filter('.error')->count()); + } + + /** + * Test that setting a Null date redirect to an error page + * + * @param Form $form + * @depends testNewReportPage + */ + public function testInvalidDate(Form $form) + { + $client = $this->getAuthenticatedClient(); + $this->markTestSkipped("This test raise an error since symfony 2.7. " + . "The user is not correctly reloaded from database."); + $filledForm = $this->fillCorrectForm($form); + $filledForm->get('chill_reportbundle_report[date]')->setValue('invalid date value'); + + $crawler = $client->submit($filledForm); + + $this->assertFalse($client->getResponse()->isRedirect()); + $this->assertGreaterThan(0, $crawler->filter('.error')->count()); + } + + /** + * Test that a incorrect value in user will show an error page + * + * @depends testNewReportPage + * @param Form $form + */ + public function testInvalidUser(Form $form) + { + $client = $this->getAuthenticatedClient(); + $filledForm = $this->fillCorrectForm($form); + $select = $filledForm->get('chill_reportbundle_report[user]') + ->disableValidation() + ->setValue(-1); + + $crawler = $client->submit($filledForm); + + $this->assertFalse($client->getResponse()->isRedirect()); + $this->assertGreaterThan(0, $crawler->filter('.error')->count()); + } + + /** + * Test the creation of a report + * + * depends testNewReportPage + * param Form $form + */ + public function testValidCreate() + { + $client = $this->getAuthenticatedClient(); + //$this->markTestSkipped("This test raise an error since symfony 2.7. " + // . "The user is not correctly reloaded from database."); + $addForm = $this->getReportForm(self::$person, self::$group, $client); + $filledForm = $this->fillCorrectForm($addForm); + $c = $client->submit($filledForm); + + $this->assertTrue($client->getResponse()->isRedirect(), + "The next page is a redirection to the new report's view page"); + $client->followRedirect(); + + $this->assertRegExp("|/fr/person/".static::$person->getId()."/report/[0-9]*/view$|", + $client->getHistory()->current()->getUri(), + "The next page is a redirection to the new report's view page"); + + $matches = array(); + preg_match('|/report/([0-9]*)/view$|', + $client->getHistory()->current()->getUri(), $matches); + + return $matches[1]; + } + + + /** + * @depends testValidCreate + * @param int $reportId + */ + public function testList($reportId) + { + $client = $this->getAuthenticatedClient(); + $crawler = $client->request('GET', sprintf('/fr/person/%s/report/list', + static::$person->getId())); + + $this->assertTrue($client->getResponse()->isSuccessful()); + + $linkSee = $crawler->filter('.bt-view')->links(); + $this->assertGreaterThan(0, count($linkSee)); + $this->assertRegExp(sprintf('|/fr/person/%s/report/[0-9]*/view$|', + static::$person->getId(), $reportId), $linkSee[0]->getUri()); + + $linkUpdate = $crawler->filter('.bt-update')->links(); + $this->assertGreaterThan(0, count($linkUpdate)); + $this->assertRegExp(sprintf('|/fr/person/%s/report/[0-9]*/edit$|', + static::$person->getId(), $reportId), $linkUpdate[0]->getUri()); + + } + + /** + * Test the view of a report + * + * @depends testValidCreate + * @param int $reportId + */ + public function testView($reportId) + { + $client = $this->getAuthenticatedClient(); + $client->request('GET', + sprintf('/fr/person/%s/report/%s/view', static::$person->getId(), $reportId)); + + $this->assertTrue($client->getResponse()->isSuccessful(), + 'the page is shown'); + } + + /** + * test the update form + * + * @depends testValidCreate + * @param int $reportId + */ + public function testUpdate($reportId) + { + $client = $this->getAuthenticatedClient(); + $crawler = $client->request('GET', + sprintf('/fr/person/%s/report/%s/edit', static::$person->getId(), $reportId)); + + $this->assertTrue($client->getResponse()->isSuccessful()); + + $form = $crawler + ->selectButton('Enregistrer le rapport') + ->form(); + + $form->get('chill_reportbundle_report[date]')->setValue( + (new \DateTime('yesterday'))->format('d-m-Y')); + + $client->submit($form); + + $this->assertTrue($client->getResponse()->isRedirect( + sprintf('/fr/person/%s/report/%s/view', + static::$person->getId(), $reportId))); + + $this->assertEquals(new \DateTime('yesterday'), static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('ChillReportBundle:Report') + ->find($reportId) + ->getDate()); + } + +} diff --git a/src/Bundle/ChillReport/Tests/DependencyInjection/ChillReportExtensionTest.php b/src/Bundle/ChillReport/Tests/DependencyInjection/ChillReportExtensionTest.php new file mode 100644 index 000000000..beb246ea0 --- /dev/null +++ b/src/Bundle/ChillReport/Tests/DependencyInjection/ChillReportExtensionTest.php @@ -0,0 +1,29 @@ + 'test')); + $customizablesEntities = static::$kernel->getContainer() + ->getParameter('chill_custom_fields.customizables_entities'); + + $reportFounded = false; + foreach ($customizablesEntities as $customizablesEntity) { + if($customizablesEntity['class'] === 'Chill\ReportBundle\Entity\Report') { + $reportFounded = true; + } + } + + if(! $reportFounded) { + throw new Exception("Class Chill\ReportBundle\Entity\Report not found in chill_custom_fields.customizables_entities", 1); + } + } +} diff --git a/src/Bundle/ChillReport/Tests/Search/ReportSearchTest.php b/src/Bundle/ChillReport/Tests/Search/ReportSearchTest.php new file mode 100644 index 000000000..ce0bd1053 --- /dev/null +++ b/src/Bundle/ChillReport/Tests/Search/ReportSearchTest.php @@ -0,0 +1,126 @@ + + * + * 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\ReportBundle\Tests\Search; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * Test for report search + * + * @author Julien Fastré + */ +class ReportSearchTest extends WebTestCase +{ + + public function testSearchExpectedDefault() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/search', array( + 'q' => '@report 2015-01-05' + )); + + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertRegExp('/Situation de logement/i', $crawler->text()); + } + + public function testNamedSearch() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/search', array( + 'q' => '@report '.(new \DateTime('tomorrow'))->format('Y-m-d'), //there should be any result for future. And future is tomorrow + 'name' => 'report' + )); + + $this->assertTrue($client->getResponse()->isSuccessful()); + } + + public function testSearchByDate() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/search', array( + 'q' => '@report date:2015-01-05' + )); + + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertRegExp('/Situation de logement/i', $crawler->text()); + } + + public function testSearchDoubleDate() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/search', array( + 'q' => '@report date:2014-05-01 2014-05-06' + )); + + $this->assertGreaterThan(0, $crawler->filter('.error')->count()); + } + + public function testSearchEmtpy() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/search', array( + 'q' => '@report ' + )); + + $this->assertGreaterThan(0, $crawler->filter('.error')->count()); + } + + /** + * Test that the user do not see unauthorized results + * + * We test for that that : + * - we do not see any unauthorized scope mention + */ + public function testUsersDoNotSeeUnauthorizedResults() + { + $clientSocial = $this->getAuthenticatedClient(); + $clientAdministrative = $this->getAuthenticatedClient('center a_administrative'); + + $params = array('q' => '@report date:2015-01-05'); + + $crawlerSocial = $clientSocial->request('GET', '/fr/search', $params); + $crawlerAdministrative = $clientAdministrative->request('GET', '/fr/search', $params); + + + $this->assertNotContains('social', $crawlerAdministrative->filter('.content') + ->text()); + $this->assertNotContains('administrative', $crawlerAdministrative->filter('.content') + ->text()); + } + + + /** + * + * @return \Symfony\Component\BrowserKit\Client + */ + private function getAuthenticatedClient($username = 'center a_social') + { + return static::createClient(array(), array( + 'PHP_AUTH_USER' => $username, + 'PHP_AUTH_PW' => 'password', + )); + } +} diff --git a/src/Bundle/ChillReport/Tests/Security/Authorization/ReportVoterTest.php b/src/Bundle/ChillReport/Tests/Security/Authorization/ReportVoterTest.php new file mode 100644 index 000000000..8fa450fb0 --- /dev/null +++ b/src/Bundle/ChillReport/Tests/Security/Authorization/ReportVoterTest.php @@ -0,0 +1,193 @@ + + * + * 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\ReportBundle\Tests\Security\Authorization; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Chill\MainBundle\Test\PrepareUserTrait; +use Chill\MainBundle\Test\PrepareCenterTrait; +use Chill\MainBundle\Test\PrepareScopeTrait; +use Chill\ReportBundle\Entity\Report; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Center; +use Chill\PersonBundle\Entity\Person; + +/** + * + * + * @author Julien Fastré + */ +class ReportVoterTest extends KernelTestCase +{ + + use PrepareUserTrait, PrepareCenterTrait, PrepareScopeTrait; + + /** + * + * @var \Chill\ReportBundle\Security\Authorization\ReportVoter + */ + protected $voter; + + /** + * + * @var \Prophecy\Prophet + */ + protected $prophet; + + public static function setUpBeforeClass() + { + + } + + public function setUp() + { + static::bootKernel(); + $this->voter = static::$kernel->getContainer() + ->get('chill.report.security.authorization.report_voter'); + $this->prophet = new \Prophecy\Prophet(); + } + + /** + * @dataProvider dataProvider + * @param type $expectedResult + * @param Report $report + * @param User $user + * @param type $action + * @param type $message + */ + public function testAccess($expectedResult, Report $report, $action, + $message, User $user = null) + { + $token = $this->prepareToken($user); + $result = $this->voter->vote($token, $report, [$action]); + $this->assertEquals($expectedResult, $result, $message); + } + + /** + * prepare a person + * + * The only properties set is the center, others properties are ignored. + * + * @param Center $center + * @return Person + */ + protected function preparePerson(Center $center) + { + return (new Person()) + ->setCenter($center) + ; + } + + /** + * prepare a token interface with correct rights + * + * if $permissions = null, user will be null (no user associated with token + * + * @param User $user + * @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface + */ + protected function prepareToken(User $user = null) + { + $token = $this->prophet->prophesize(); + $token + ->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + if ($user === NULL) { + $token->getUser()->willReturn(null); + } else { + $token->getUser()->willReturn($user); + } + + return $token->reveal(); + } + + public function dataProvider() + { + $centerA = $this->prepareCenter(1, 'center A'); + $centerB = $this->prepareCenter(2, 'center B'); + $scopeA = $this->prepareScope(1, 'scope default'); + $scopeB = $this->prepareScope(2, 'scope B'); + $scopeC = $this->prepareScope(3, 'scope C'); + + $userA = $this->prepareUser(array( + array( + 'center' => $centerA, + 'permissionsGroup' => array( + ['scope' => $scopeB, 'role' => 'CHILL_REPORT_SEE'], + ['scope' => $scopeA, 'role' => 'CHILL_REPORT_UPDATE'] + ) + ), + array( + 'center' => $centerB, + 'permissionsGroup' => array( + ['scope' => $scopeA, 'role' => 'CHILL_REPORT_SEE'], + ) + ) + + )); + + $reportA = (new Report) + ->setScope($scopeA) + ->setPerson($this->preparePerson($centerA)) + ; + $reportB = (new Report()) + ->setScope($scopeB) + ->setPerson($this->preparePerson($centerA)) + ; + $reportC = (new Report()) + ->setScope($scopeC) + ->setPerson($this->preparePerson($centerB)) + ; + + + return array( + array( + VoterInterface::ACCESS_DENIED, + $reportA, + 'CHILL_REPORT_SEE', + "assert is denied to a null user", + null + ), + array( + VoterInterface::ACCESS_GRANTED, + $reportA, + 'CHILL_REPORT_SEE', + "assert access is granted to a user with inheritance UPDATE > SEE", + $userA + ), + array( + VoterInterface::ACCESS_GRANTED, + $reportB, + 'CHILL_REPORT_SEE', + "assert access is granted to a user without inheritance", + $userA + ), + array( + VoterInterface::ACCESS_DENIED, + $reportC, + 'CHILL_REPORT_SEE', + 'assert access is denied to a report', + $userA + ) + ); + + } + + +} diff --git a/src/Bundle/ChillReport/Tests/Timeline/TimelineProviderTest.php b/src/Bundle/ChillReport/Tests/Timeline/TimelineProviderTest.php new file mode 100644 index 000000000..830600a6e --- /dev/null +++ b/src/Bundle/ChillReport/Tests/Timeline/TimelineProviderTest.php @@ -0,0 +1,187 @@ + + * + * 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\ReportBundle\Tests\Timeline; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Chill\PersonBundle\Entity\Person; +use Chill\ReportBundle\Entity\Report; +use Chill\MainBundle\Tests\TestHelper as MainTestHelper; +use Chill\MainBundle\Entity\Scope; + +/** + * Test a report is shown into timeline + * + * @author Julien Fastré + * @author Champs Libres + */ +class TimelineProviderTest extends WebTestCase +{ + + /** + * + * @var \Doctrine\ORM\EntityManager + */ + private static $em; + + /** + * + * @var Person + */ + private $person; + + /** + * + * @var Report + */ + private $report; + + /** + * Create a person with a report associated with the person + */ + public function setUp() + { + static::bootKernel(); + + static::$em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = static::$em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + + $person = (new Person(new \DateTime('2015-05-01'))) + ->setGender(Person::FEMALE_GENDER) + ->setFirstName('Nelson') + ->setLastName('Mandela') + ->setCenter($center); + static::$em->persist($person); + $this->person = $person; + + $scopesSocial = array_filter(static::$em + ->getRepository('ChillMainBundle:Scope') + ->findAll(), + function(Scope $scope) { return $scope->getName()['en'] === 'social'; }) + ; + + $report = (new Report) + ->setUser(static::$em->getRepository('ChillMainBundle:User') + ->findOneByUsername('center a_social')) + ->setDate(new \DateTime('2015-05-02')) + ->setPerson($this->person) + ->setCFGroup($this->getHousingCustomFieldsGroup()) + ->setCFData(['has_logement' => 'own_house', + 'house-desc' => 'blah blah']) + ->setScope(end($scopesSocial)); + + static::$em->persist($report); + $this->report = $report; + + + + static::$em->flush(); + + } + + /** + * Test that a report is shown in timeline + */ + public function testTimelineReport() + { + $client = static::createClient(array(), + MainTestHelper::getAuthenticatedClientOptions() + ); + + $crawler = $client->request('GET', '/fr/person/'.$this->person->getId() + .'/timeline'); + + $this->assertTrue($client->getResponse()->isSuccessful(), + 'The page timeline is loaded successfully'); + $this->assertContains('a ajouté un rapport', $crawler->text(), + 'the page contains the text "a publié un rapport"'); + } + + public function testTimelineReportWithSummaryField() + { + //load the page + $client = static::createClient(array(), + MainTestHelper::getAuthenticatedClientOptions() + ); + + $crawler = $client->request('GET', '/fr/person/'.$this->person->getId() + .'/timeline'); + + //performs tests + $this->assertTrue($client->getResponse()->isSuccessful(), + 'The page timeline is loaded successfully'); + $this->assertGreaterThan(0, $crawler->filter('.report_entry .summary') + ->count(), + 'the page contains a .report .summary element'); + $this->assertContains('blah blah', $crawler->filter('.report_entry .summary') + ->text(), + 'the page contains the text "blah blah"'); + $this->assertContains('Propriétaire', $crawler->filter('.report_entry .summary') + ->text(), + 'the page contains the mention "Propriétaire"'); + } + + public function testReportIsNotVisibleToUngrantedUsers() + { + $client = static::createClient(array(), + MainTestHelper::getAuthenticatedClientOptions('center a_administrative') + ); + + $crawler = $client->request('GET', '/fr/person/'.$this->person->getId() + .'/timeline'); + + $this->assertEquals(0, $crawler->filter('.report_entry .summary') + ->count(), + 'the page does not contains a .report .summary element'); + } + + /** + * get a random custom fields group + * + * @return \Chill\CustomFieldsBundle\Entity\CustomFieldsGroup + */ + private function getHousingCustomFieldsGroup() + { + $groups = static::$em + ->getRepository('ChillCustomFieldsBundle:CustomFieldsGroup') + ->findAll(); + + foreach ($groups as $group) { + if ($group->getName()['fr'] === 'Situation de logement') { + return $group; + } + } + + return $groups[rand(0, count($groups) -1)]; + } + + + + public function tearDown() + { + //static::$em->refresh($this->person); + //static::$em->refresh($this->report); + // static::$em->remove($this->person); + //static::$em->remove($this->report); + } +} diff --git a/src/Bundle/ChillReport/Tests/bootstrap.php b/src/Bundle/ChillReport/Tests/bootstrap.php new file mode 100644 index 000000000..2437ecd2d --- /dev/null +++ b/src/Bundle/ChillReport/Tests/bootstrap.php @@ -0,0 +1,7 @@ + + * + * 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\ReportBundle\Timeline; + +use Chill\MainBundle\Timeline\TimelineProviderInterface; +use Doctrine\ORM\EntityManager; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\Role; +use Doctrine\ORM\Mapping\ClassMetadata; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Entity\Scope; +use Chill\CustomFieldsBundle\Service\CustomFieldsHelper; +use Chill\ReportBundle\Entity\Report; + +/** + * Provide report for inclusion in timeline + * + * @author Julien Fastré + * @author Champs Libres + */ +class TimelineReportProvider implements TimelineProviderInterface +{ + + /** + * + * @var EntityManager + */ + protected $em; + + /** + * + * @var AuthorizationHelper + */ + protected $helper; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + /** + * + * @var CustomFieldsHelper + */ + protected $customFieldsHelper; + + /** + * @var + */ + protected $showEmptyValues; + + /** + * TimelineReportProvider constructor. + * + * @param EntityManager $em + * @param AuthorizationHelper $helper + * @param TokenStorageInterface $storage + * @param CustomFieldsHelper $customFieldsHelper + * @param $showEmptyValues + */ + public function __construct( + EntityManager $em, + AuthorizationHelper $helper, + TokenStorageInterface $storage, + CustomFieldsHelper $customFieldsHelper, + $showEmptyValues + ) + { + $this->em = $em; + $this->helper = $helper; + + if (!$storage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User) + { + throw new \RuntimeException('A user should be authenticated !'); + } + + $this->user = $storage->getToken()->getUser(); + $this->customFieldsHelper = $customFieldsHelper; + $this->showEmptyValues = $showEmptyValues; + } + + /** + * + * {@inheritDoc} + */ + public function fetchQuery($context, array $args) + { + $this->checkContext($context); + + $metadataReport = $this->em->getClassMetadata('ChillReportBundle:Report'); + $metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person'); + + return array( + 'id' => $metadataReport->getTableName() + .'.'.$metadataReport->getColumnName('id'), + 'type' => 'report', + 'date' => $metadataReport->getTableName() + .'.'.$metadataReport->getColumnName('date'), + 'FROM' => $this->getFromClause($metadataReport, $metadataPerson), + 'WHERE' => $this->getWhereClause($metadataReport, $metadataPerson, + $args['person']) + ); + } + + private function getWhereClause(ClassMetadata $metadataReport, + ClassMetadata $metadataPerson, Person $person) + { + $role = new Role('CHILL_REPORT_SEE'); + $reachableCenters = $this->helper->getReachableCenters($this->user, + $role); + $associationMapping = $metadataReport->getAssociationMapping('person'); + + // we start with reports having the person_id linked to person + // (currently only context "person" is supported) + $whereClause = sprintf('%s = %d', + $associationMapping['joinColumns'][0]['name'], + $person->getId()); + + // we add acl (reachable center and scopes) + $centerAndScopeLines = array(); + foreach ($reachableCenters as $center) { + $reachablesScopesId = array_map( + function(Scope $scope) { return $scope->getId(); }, + $this->helper->getReachableScopes($this->user, $role, + $person->getCenter()) + ); + + $centerAndScopeLines[] = sprintf('(%s = %d AND %s IN (%s))', + $metadataPerson->getTableName().'.'. + $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'], + $center->getId(), + $metadataReport->getTableName().'.'. + $metadataReport->getAssociationMapping('scope')['joinColumns'][0]['name'], + implode(',', $reachablesScopesId)); + + } + $whereClause .= ' AND ('.implode(' OR ', $centerAndScopeLines).')'; + + return $whereClause; + } + + private function getFromClause(ClassMetadata $metadataReport, + ClassMetadata $metadataPerson) + { + $associationMapping = $metadataReport->getAssociationMapping('person'); + + return $metadataReport->getTableName().' JOIN ' + .$metadataPerson->getTableName().' ON ' + .$metadataPerson->getTableName().'.'. + $associationMapping['joinColumns'][0]['referencedColumnName'] + .' = ' + .$associationMapping['joinColumns'][0]['name'] + ; + } + + /** + * + * {@inheritDoc} + */ + public function getEntities(array $ids) + { + $reports = $this->em->getRepository('ChillReportBundle:Report') + ->findBy(array('id' => $ids)); + + $result = array(); + foreach($reports as $report) { + $result[$report->getId()] = $report; + } + + return $result; + } + + /** + * + * {@inheritDoc} + */ + public function getEntityTemplate($entity, $context, array $args) + { + $this->checkContext($context); + + return array( + 'template' => 'ChillReportBundle:Timeline:report_person_context.html.twig', + 'template_data' => array( + 'report' => $entity, + 'custom_fields_in_summary' => $this->getFieldsToRender($entity, $context), + 'person' => $args['person'], + 'user' => $entity->getUser() + ) + ); + } + + protected function getFieldsToRender(Report $entity, $context, array $args = array()) + { + //gather all custom fields which should appears in summary + $gatheredFields = array(); + + if (array_key_exists('summary_fields', $entity->getCFGroup()->getOptions())) { + // keep in memory title + $title = null; + $subtitle = null; + + foreach ($entity->getCFGroup()->getCustomFields() as $customField) { + if (in_array($customField->getSlug(), + $entity->getCFGroup()->getOptions()['summary_fields'])) { + // if we do not want to show empty values + if ($this->showEmptyValues === false) { + if ($customField->getType() === 'title') { + $options = $customField->getOptions(); + switch($options['type']) { + case 'title': $title = $customField; break; + case 'subtitle': $subtitle = $customField; break; + } + } else { + if ($this->customFieldsHelper->isEmptyValue($entity->getCFData(), $customField) + === false) { + if ($title !== NULL) { + $gatheredFields[] = $title; + $title = null; + } + if ($subtitle !== NULL) { + $gatheredFields[] = $subtitle; + $subtitle = null; + } + $gatheredFields[] = $customField; + } + } + } else { + $gatheredFields[] = $customField; + } + } + } + } + + return $gatheredFields; + + } + + /** + * + * {@inheritDoc} + */ + public function supportsType($type) + { + return $type === 'report'; + } + + /** + * check if the context is supported + * + * @param string $context + * @throws \LogicException if the context is not supported + */ + private function checkContext($context) + { + if ($context !== 'person') { + throw new \LogicException("The context '$context' is not " + . "supported. Currently only 'person' is supported"); + } + } + +} diff --git a/src/Bundle/ChillReport/apigen.neon b/src/Bundle/ChillReport/apigen.neon new file mode 100644 index 000000000..6fd31b419 --- /dev/null +++ b/src/Bundle/ChillReport/apigen.neon @@ -0,0 +1,11 @@ +# configuration for apigen + +source: + - . + +exclude: + - vendor/* + - Test* + - Resources/test + +title: Chill Report Bundle diff --git a/src/Bundle/ChillReport/composer.json b/src/Bundle/ChillReport/composer.json new file mode 100644 index 000000000..a5c17a2ee --- /dev/null +++ b/src/Bundle/ChillReport/composer.json @@ -0,0 +1,41 @@ +{ + "name": "chill-project/report", + "description": "The bundle for reports", + "type": "symfony-bundle", + "keywords" : ["chill", "social work"], + "license": "AGPL-3.0", + "homepage" : "https://github.com/Chill-project/Report", + "autoload": { + "psr-4": { "Chill\\ReportBundle\\": "" } + }, + "autoload-dev": { + "classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ] + }, + "authors" : [ + { + "name": "Champs-Libres", + "email": "info@champs-libres.coop", + "homepage": "http://www.champs-libres.coop" + } + ], + "require": { + }, + "require-dev": { + }, + "scripts": { + "post-install-cmd": [ + "ComposerBundleMigration\\Composer\\Migrations::synchronizeMigrations", + "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap" + ], + "post-update-cmd": [ + "ComposerBundleMigration\\Composer\\Migrations::synchronizeMigrations", + "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap" + ] + }, + "extra": { + "app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations", + "symfony-app-dir": "Tests/Fixtures/App/app" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/src/Bundle/ChillReport/config/routes.yaml b/src/Bundle/ChillReport/config/routes.yaml new file mode 100644 index 000000000..8b15f0077 --- /dev/null +++ b/src/Bundle/ChillReport/config/routes.yaml @@ -0,0 +1,51 @@ +report_select_type: + path: /{_locale}/person/{person_id}/report/select/type/for/creation + defaults: { _controller: "ChillReportBundle:Report:selectReportType" } + options: + menus: + person: + order: 100 + label: Add a report +report_new: + path: /{_locale}/person/{person_id}/report/cfgroup/{cf_group_id}/new + defaults: { _controller: "ChillReportBundle:Report:new" } + +report_create: + path: /{_locale}/person/{person_id}/report/cfgroup/{cf_group_id}/create + defaults: { _controller: "ChillReportBundle:Report:create" } + methods: [POST] + +report_list: + path: /{_locale}/person/{person_id}/report/list + defaults: { _controller: "ChillReportBundle:Report:list" } + options: + menus: + person: + order: 101 + label: Report list +report_view: + path: /{_locale}/person/{person_id}/report/{report_id}/view + defaults: { _controller: "ChillReportBundle:Report:view" } + +report_edit: + path: /{_locale}/person/{person_id}/report/{report_id}/edit + defaults: { _controller: "ChillReportBundle:Report:edit" } + +report_update: + path: /{_locale}/person/{person_id}/report/{report_id}/update + defaults: { _controller: "ChillReportBundle:Report:update" } + methods: [POST, PUT] + +report_export_list: + path: /{_locale}/export/report/cfgroup/{cf_group_id} + defaults: { _controller: "ChillReportBundle:Report:export" } + + +report_export_select_type: + path: /{_locale}/export/report/select/type + defaults: {_controller: "ChillReportBundle:Report:selectReportTypeForExport" } + options: + menus: + export: + order: 100 + label: Export reports diff --git a/src/Bundle/ChillReport/config/services.yaml b/src/Bundle/ChillReport/config/services.yaml new file mode 100644 index 000000000..fec944ff5 --- /dev/null +++ b/src/Bundle/ChillReport/config/services.yaml @@ -0,0 +1,46 @@ +services: +# chill_report.example: +# class: Chill\ReportBundle\Example +# arguments: [@service_id, "plain_value", %parameter%] +services: + + chill.report.search: + class: Chill\ReportBundle\Search\ReportSearch + arguments: + - '@doctrine.orm.entity_manager' + - '@chill.main.security.authorization.helper' + - '@security.token_storage' + calls: + - [setContainer, ["@service_container"]] + tags: + - { name: chill.search, alias: 'report' } + + chill.report.timeline: + class: Chill\ReportBundle\Timeline\TimelineReportProvider + arguments: + - '@doctrine.orm.entity_manager' + - '@chill.main.security.authorization.helper' + - '@security.token_storage' + - '@chill.custom_field.helper' + - '%chill_custom_fields.show_empty_values%' + tags: + - { name: chill.timeline, context: 'person' } + + chill.report.security.authorization.report_voter: + class: Chill\ReportBundle\Security\Authorization\ReportVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } + - { name: chill.role } + + chill.report.form.report_type: + class: Chill\ReportBundle\Form\ReportType + arguments: + - "@chill.main.security.authorization.helper" + - "@security.token_storage" + - "@chill.main.helper.translatable_string" + - "@doctrine.orm.entity_manager" + tags: + - { name: form.type, alias: chill_reportbundle_report } + \ No newline at end of file diff --git a/src/Bundle/ChillReport/config/services/controller.yaml b/src/Bundle/ChillReport/config/services/controller.yaml new file mode 100644 index 000000000..0eb58e45a --- /dev/null +++ b/src/Bundle/ChillReport/config/services/controller.yaml @@ -0,0 +1,7 @@ +services: + Chill\ReportBundle\Controller\ReportController: + arguments: + $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + $paginator: '@Chill\MainBundle\Pagination\PaginatorFactory' + tags: ['controller.service_arguments'] diff --git a/src/Bundle/ChillReport/config/services/export.yaml b/src/Bundle/ChillReport/config/services/export.yaml new file mode 100644 index 000000000..4b9d7a7e7 --- /dev/null +++ b/src/Bundle/ChillReport/config/services/export.yaml @@ -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/src/Bundle/ChillReport/config/services/fixtures.yaml b/src/Bundle/ChillReport/config/services/fixtures.yaml new file mode 100644 index 000000000..6362bbfe7 --- /dev/null +++ b/src/Bundle/ChillReport/config/services/fixtures.yaml @@ -0,0 +1,4 @@ +services: + Chill\ReportBundle\DataFixtures\ORM\: + resource: ../../DataFixtures/ORM + tags: [ 'doctrine.fixture.orm' ] \ No newline at end of file diff --git a/src/Bundle/ChillReport/config/validation.yaml b/src/Bundle/ChillReport/config/validation.yaml new file mode 100644 index 000000000..e6920f438 --- /dev/null +++ b/src/Bundle/ChillReport/config/validation.yaml @@ -0,0 +1,5 @@ +Chill\ReportBundle\Entity\Report: + properties: + date: + - NotNull: ~ + - Date: ~ diff --git a/src/Bundle/ChillReport/console.sh b/src/Bundle/ChillReport/console.sh new file mode 100755 index 000000000..609ce9cde --- /dev/null +++ b/src/Bundle/ChillReport/console.sh @@ -0,0 +1 @@ +php Tests/Fixtures/App/console $1 $2 $3 $4 $5 \ No newline at end of file diff --git a/src/Bundle/ChillReport/migrations/Version20141129012050.php b/src/Bundle/ChillReport/migrations/Version20141129012050.php new file mode 100644 index 000000000..3108024f8 --- /dev/null +++ b/src/Bundle/ChillReport/migrations/Version20141129012050.php @@ -0,0 +1,30 @@ +addSql("CREATE SEQUENCE Report_id_seq INCREMENT BY 1 MINVALUE 1 START 1;"); + $this->addSql("CREATE TABLE Report (id INT NOT NULL, user_id INT DEFAULT NULL, person_id INT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, scope VARCHAR(255) DEFAULT NULL, cFData JSON NOT NULL, cFGroup_id INT DEFAULT NULL, PRIMARY KEY(id));"); + $this->addSql("CREATE INDEX IDX_C38372B2A76ED395 ON Report (user_id);"); + $this->addSql("CREATE INDEX IDX_C38372B2217BBB47 ON Report (person_id);"); + $this->addSql("CREATE INDEX IDX_C38372B216D2C9F0 ON Report (cFGroup_id);"); + $this->addSql("ALTER TABLE Report ADD CONSTRAINT FK_C38372B2A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE;"); + $this->addSql("ALTER TABLE Report ADD CONSTRAINT FK_C38372B2217BBB47 FOREIGN KEY (person_id) REFERENCES Person (id) NOT DEFERRABLE INITIALLY IMMEDIATE;"); + $this->addSql("ALTER TABLE Report ADD CONSTRAINT FK_C38372B216D2C9F0 FOREIGN KEY (cFGroup_id) REFERENCES CustomFieldsGroup (id) NOT DEFERRABLE INITIALLY IMMEDIATE;"); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + + } +} diff --git a/src/Bundle/ChillReport/migrations/Version20150622233319.php b/src/Bundle/ChillReport/migrations/Version20150622233319.php new file mode 100644 index 000000000..3e0353b41 --- /dev/null +++ b/src/Bundle/ChillReport/migrations/Version20150622233319.php @@ -0,0 +1,116 @@ +container = $container; + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', + 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE report ADD scope_id INT DEFAULT NULL'); + + /* + * Before the upgrade to symfony version 4, this code worked. + * + * But it doesn't work any more after the migration and currently this + * code should note be necessary. + * + * This code is kept for reference, and may be re-activated if needed, but + * the probability that this will happens is near 0 + * + + //add a default scope + $scopes = $this->container->get('doctrine.orm.default_entity_manager') + ->getRepository('ChillMainBundle:Scope') + ->findAll(); + + if (count($scopes) > 0) { + $defaultScopeId = $scopes[0]->getId(); + } else { + //check if there are data in report table + $nbReports = $this->container->get('doctrine.orm.default_entity_manager') + ->createQuery('SELECT count(r.id) FROM ChillReportBundle:Report r') + ->getSingleScalarResult(); + + if ($nbReports > 0) { + //create a default scope + $scope = new Scope(); + //create name according to installed languages + $locales = $this->container + ->getParameter('chill_main.available_languages'); + $names = array(); + foreach($locales as $locale) { + $names[$locale] = 'default'; + } + $scope->setName($names); + //persist + $this->container->get('doctrine.orm.default_entity_manager') + ->persist($scope); + $this->container->get('doctrine.orm.default_entity_manager') + ->flush(); + //default scope is the newly-created one + $defaultScopeId = $scope->getId(); + } + }*/ + + $this->addSql('ALTER TABLE report DROP scope'); //before this migration, scope was never used + $this->addSql('ALTER TABLE report ADD CONSTRAINT FK_report_scope ' + . 'FOREIGN KEY (scope_id) ' + . 'REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + if (isset($defaultScopeId)){ + $this->addSql('UPDATE report SET scope_id = :id', array( + 'id' => $defaultScopeId + )); + } + $this->addSql('ALTER TABLE report ALTER COLUMN scope_id SET NOT NULL'); + $this->addSql('CREATE INDEX IDX_report_scope ON report (scope_id)'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', + 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE Report DROP CONSTRAINT FK_report_scope'); + $this->addSql('DROP INDEX IDX_report_scope'); + $this->addSql('ALTER TABLE Report ADD scope VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE Report DROP scope_id'); + } +} diff --git a/src/Bundle/ChillReport/phpunit.xml.dist b/src/Bundle/ChillReport/phpunit.xml.dist new file mode 100644 index 000000000..5e67beade --- /dev/null +++ b/src/Bundle/ChillReport/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + ./Tests + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + + + + + diff --git a/src/Bundle/ChillReport/translations/messages.fr.yml b/src/Bundle/ChillReport/translations/messages.fr.yml new file mode 100644 index 000000000..9cd2d5d8c --- /dev/null +++ b/src/Bundle/ChillReport/translations/messages.fr.yml @@ -0,0 +1,55 @@ +'Report edit': "Edition d'un rapport" +'Save report': "Enregistrer le rapport" +'Reset report': "Remise à zéro" +'Add a report': "Ajout d'un rapport" +'Add report': 'Ajouter le rapport' +'Create a new report': 'Créer un nouveau rapport' +'Report view': "Détails d'un rapport" +'Update the report': 'Modifier le rapport' +'Report list': 'Liste des rapports' +Details: Détails +Person: Personne +Scope: Cercle +Date: Date +User: Utilisateur +'Report type': 'Type de rapport' +'View the report': "Voir le rapport" +Report data: Données du rapport +'Report view : %name%': 'Rapport : %name%' +Report: Rapport +No report registered for this person.: Aucun rapport pour cette personne. + +#Flash messages +'Success : report created!': "Succès : le rapport a bien été créé !" +'The form is not valid. The report has not been created !': "Le formulaire comporte des erreurs, le rapport n'a pas été créé" +'Success : report updated!': "Succès : le rapport a bien été mis à jour !" +'The form is not valid. The report has not been updated !': "Le formulaire comporte des erreurs, le rapport n'a pas été mis à jour" + +#Exception messags +'Unable to find this report.': Rapport introuvable. +'This is not the report of the person.': "La personne et le rapport sélectionnés ne sont pas associés" + + +#search +'You may not set a date argument and a date in default': Vous avez introduit deux dates, l'une avec l'argument date et l'autre en zone de recherche par défaut. Merci d'indiquer l'un ou l'autre +'You must provide either a date:YYYY-mm-dd argument or a YYYY-mm-dd default search': Merci d'indiquer soit un argument date:YYYY-mm-dd, soit une date dans la recherche par défaut. +'Reports search results': Recherche dans les rapports +'%total% reports matching the search "%pattern%"': '{0} Aucun rapport ne correspond à la recherche "%pattern%" | {1} Un rapport correspond à la recherche "%pattern%" | ]1,Inf]%total% rapports correspondent à la recherche "%pattern%"' + +#timeline +'%user% has filled a %report_label% report': "%user% a ajouté un rapport '%report_label%'" + +#roles +CHILL_REPORT_UPDATE: Modifier les rapports +CHILL_REPORT_SEE: Voir les rapports +CHILL_REPORT_CREATE: Créer des rapports +CHILL_REPORT_LISTS: Liste 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/src/Bundle/ChillReport/translations/messages.nl.yml b/src/Bundle/ChillReport/translations/messages.nl.yml new file mode 100644 index 000000000..54bd178ff --- /dev/null +++ b/src/Bundle/ChillReport/translations/messages.nl.yml @@ -0,0 +1,31 @@ +'Report edit': Dossier uitgave +'Save report': Bewaar consultatiegegevens +'Reset report': Verwijder consultatiegegevens +'Add a report': Voeg een consultatierapport toe +'Add report': 'Voeg rapport toe' +'Create a new report': 'Maak een nieuw consultatieblad aan' +'Report view': "Details van de consultatiegegevens" +'Update the report': 'Vul dossier aan' +'Report list': 'Overzicht van de consultaties' +Details: Details +Person: Person +Scope: Scope +Date: Datum +User: Gebruiker +'Report type': 'Soort of verslag' +'View the report': "Bekijk de consultatiegegevens" + +#Flash messages +'Success : report created!': "De consultatiegegevens zijn correct ingevuld!" +'The form is not valid. The report has not been created !': "De gegevens zijn niet correct, er wordt geen rapport gemaakt" +'Success : report updated!': "Update is gelukt" +'The form is not valid. The report has not been updated !': "Update is mislukt, probeer opnieuw." + +#Exception messags +'Unable to find this report.': Dossier is onvindbaar +'This is not the report of the person.': "Dit is niet het dossier van deze person" + +'You are going to leave a page with unsubmitted data. Are you sure you want to leave ?': 'U bent aan te vertrekken een pagina die veranderdt datas behoudt. Bent u er zeker van dat u wil vertrekken ?' + +#timeline +'%user% has filled a %report_label% report on %date%': "%user% heeft een rapport '%report_label%' toegevoegd om %date%"