diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea2567ddd..877e63b85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ services: before_script: - composer config github-oauth.github.com $GITHUB_TOKEN - - composer install --no-interaction --ignore-platform-reqs + - composer install --no-interaction - cp Tests/Fixtures/App/app/config/parameters.gitlab-ci.yml Tests/Fixtures/App/app/config/parameters.yml - php Tests/Fixtures/App/app/console.php --env=test cache:warmup - php Tests/Fixtures/App/app/console.php doctrine:migrations:migrate --env=test --no-interaction diff --git a/Controller/PersonAddressController.php b/Controller/PersonAddressController.php new file mode 100644 index 000000000..f76719344 --- /dev/null +++ b/Controller/PersonAddressController.php @@ -0,0 +1,259 @@ +, + * + * 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\PersonBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Form\Type\AddressType; +use Chill\MainBundle\Entity\Address; +use Doctrine\Common\Collections\Criteria; +use Symfony\Component\HttpFoundation\Request; + +/** + * Controller for addresses associated with person + * + * @author Julien Fastré + * @author Champs Libres + */ +class PersonAddressController extends Controller +{ + + public function listAction($person_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person, + "You are not allowed to edit this person."); + + return $this->render('ChillPersonBundle:Address:list.html.twig', array( + 'person' => $person + )); + } + + public function newAction($person_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + "You are not allowed to edit this person."); + + $address = new Address(); + + $form = $this->createCreateForm($person, $address); + + return $this->render('ChillPersonBundle:Address:new.html.twig', array( + 'person' => $person, + 'form' => $form->createView() + )); + } + + public function createAction($person_id, Request $request) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + "You are not allowed to edit this person."); + + $address = new Address(); + + $form = $this->createCreateForm($person, $address); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $address = $form->getData(); + $person->addAddress($address); + + $em = $this->getDoctrine()->getManager(); + $em->flush(); + + $this->addFlash('success', + $this->get('translator')->trans('The new address was created successfully') + ); + + return $this->redirectToRoute('chill_person_address_list', array( + 'person_id' => $person->getId() + )); + } + + return $this->render('ChillPersonBundle:Address:new.html.twig', array( + 'person' => $person, + 'form' => $form->createView() + )); + } + + public function editAction($person_id, $address_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + "You are not allowed to edit this person."); + + $address = $this->findAddressById($person, $address_id); + + $form = $this->createEditForm($person, $address); + + return $this->render('ChillPersonBundle:Address:edit.html.twig', array( + 'person' => $person, + 'address' => $address, + 'form' => $form->createView() + )); + + + } + + public function updateAction($person_id, $address_id, Request $request) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + "You are not allowed to edit this person."); + + $address = $this->findAddressById($person, $address_id); + + $form = $this->createEditForm($person, $address); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager() + ->flush(); + + $this->addFlash('success', $this->get('translator')->trans( + "The address has been successfully updated")); + + return $this->redirectToRoute('chill_person_address_list', array( + 'person_id' => $person->getId() + )); + } + + return $this->render('ChillPersonBundle:Address:edit.html.twig', array( + 'person' => $person, + 'address' => $address, + 'form' => $form->createView() + )); + } + + /** + * + * @param Person $person + * @param Address $address + * @return \Symfony\Component\Form\Form + */ + protected function createEditForm(Person $person, Address $address) + { + $form = $this->createForm(AddressType::class, $address, array( + 'method' => 'POST', + 'action' => $this->generateUrl('chill_person_address_update', array( + 'person_id' => $person->getId(), + 'address_id' => $address->getId() + )) + )); + + $form->add('submit', 'submit', array( + 'label' => 'Submit' + )); + + return $form; + } + + /** + * + * @param Person $person + * @param Address $address + * @return \Symfony\Component\Form\Form + */ + protected function createCreateForm(Person $person, Address $address) + { + $form = $this->createForm(AddressType::class, $address, array( + 'method' => 'POST', + 'action' => $this->generateUrl('chill_person_address_create', array( + 'person_id' => $person->getId() + )) + )); + + $form->add('submit', 'submit', array( + 'label' => 'Submit' + )); + + return $form; + } + + /** + * + * @param Person $person + * @param int $address_id + * @return Address + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the address id does not exists or is not associated with given person + */ + protected function findAddressById(Person $person, $address_id) + { + // filtering address + $criteria = Criteria::create() + ->where(Criteria::expr()->eq('id', $address_id)) + ->setMaxResults(1); + $addresses = $person->getAddresses()->matching($criteria); + + if (count($addresses) === 0) { + throw $this->createNotFoundException("Address with id $address_id " + . "matching person $person_id not found "); + } + + return $addresses->first(); + } +} diff --git a/Controller/PersonController.php b/Controller/PersonController.php index 3c461d1ba..655bbad1b 100644 --- a/Controller/PersonController.php +++ b/Controller/PersonController.php @@ -75,7 +75,7 @@ class PersonController extends Controller $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, 'You are not allowed to edit this person'); - $form = $this->createForm(new PersonType(), $person, + $form = $this->createForm(PersonType::class, $person, array( "action" => $this->generateUrl('chill_person_general_update', array("person_id" => $person_id)), @@ -98,7 +98,7 @@ class PersonController extends Controller $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, 'You are not allowed to edit this person'); - $form = $this->createForm(new PersonType(), $person, + $form = $this->createForm(PersonType::class, $person, array("cFGroup" => $this->getCFGroup())); if ($request->getMethod() === 'POST') { @@ -302,7 +302,9 @@ class PersonController extends Controller $dql = 'SELECT p from ChillPersonBundle:Person p WHERE ' . 'LOWER(p.firstName) LIKE LOWER(:firstName)' - . ' OR LOWER(p.lastName) LIKE LOWER(:lastName)'; + . ' OR LOWER(p.lastName) LIKE LOWER(:lastName)' + . ' OR LOWER(p.firstName) LIKE LOWER(:lastName)' + . ' OR LOWER(p.lastName) LIKE LOWER(:firstName)'; $query->setParameter('firstName', $form['firstName']->getData()) ->setParameter('lastName', $form['lastName']->getData()); diff --git a/DataFixtures/ORM/LoadPeople.php b/DataFixtures/ORM/LoadPeople.php index fedff5a4d..63b0c53e5 100644 --- a/DataFixtures/ORM/LoadPeople.php +++ b/DataFixtures/ORM/LoadPeople.php @@ -26,6 +26,8 @@ use Doctrine\Common\DataFixtures\OrderedFixtureInterface; use Doctrine\Common\Persistence\ObjectManager; use Chill\PersonBundle\Entity\Person; use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes; +use Chill\MainBundle\Entity\Address; /** * Load people into database @@ -38,6 +40,13 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con use \Symfony\Component\DependencyInjection\ContainerAwareTrait; + protected $faker; + + public function __construct() + { + $this->faker = \Faker\Factory::create('fr_FR'); + } + public function prepare() { //prepare days, month, years @@ -114,12 +123,27 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con $firstName = $this->firstNamesFemale[array_rand($this->firstNamesFemale)]; } + // add an address on 80% of the created people + if (rand(0,100) < 80) { + $address = $this->getRandomAddress(); + // on 30% of those person, add multiple addresses + if (rand(0,10) < 4) { + $address = array( + $address, + $this->getRandomAddress() + ); + } + } else { + $address = null; + } + $person = array( 'FirstName' => $firstName, 'LastName' => $lastName, 'Gender' => $sex, 'Nationality' => (rand(0,100) > 50) ? NULL: 'BE', 'center' => (rand(0,1) == 0) ? 'centerA': 'centerB', + 'Address' => $address, 'maritalStatus' => $this->maritalStatusRef[array_rand($this->maritalStatusRef)] ); @@ -142,10 +166,18 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'Email' => "Email d'un ami: roger@tt.com", 'CountryOfBirth' => 'BE', 'Nationality' => 'BE', - 'CFData' => array() + 'CFData' => array(), + 'Address' => null ), $specific); } + /** + * create a new person from array data + * + * @param array $person + * @param ObjectManager $manager + * @throws \Exception + */ private function addAPerson(array $person, ObjectManager $manager) { $p = new Person(); @@ -164,13 +196,51 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con $value = $this->getReference($value); break; } - call_user_func(array($p, 'set'.$key), $value); + + //try to add the data using the setSomething function, + // if not possible, fallback to addSomething function + if (method_exists($p, 'set'.$key)) { + call_user_func(array($p, 'set'.$key), $value); + } elseif (method_exists($p, 'add'.$key)) { + // if we have a "addSomething", we may have multiple items to add + // so, we set the value in an array if it is not an array, and + // will call the function addSomething multiple times + if (!is_array($value)) { + $value = array($value); + } + + foreach($value as $v) { + if ($v !== NULL) { + call_user_func(array($p, 'add'.$key), $v); + } + } + + } } $manager->persist($p); echo "add person'".$p->__toString()."'\n"; } + /** + * Creata a random address + * + * @return Address + */ + private function getRandomAddress() + { + return (new Address()) + ->setStreetAddress1($this->faker->streetAddress) + ->setStreetAddress2( + rand(0,9) > 5 ? $this->faker->streetAddress : '' + ) + ->setPostcode($this->getReference( + LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)] + )) + ->setValidFrom($this->faker->dateTimeBetween('-5 years')) + ; + } + private function getCountry($countryCode) { if ($countryCode === NULL) { diff --git a/DependencyInjection/ChillPersonExtension.php b/DependencyInjection/ChillPersonExtension.php index cc3c593be..733ff9e27 100644 --- a/DependencyInjection/ChillPersonExtension.php +++ b/DependencyInjection/ChillPersonExtension.php @@ -32,10 +32,25 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac // set configuration for validation $container->setParameter('chill_person.validation.birtdate_not_before', $config['validation']['birthdate_not_after']); + + $this->handlePersonFieldsParameters($container, $config['person_fields']); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); } + + private function handlePersonFieldsParameters(ContainerBuilder $container, $config) + { + if (array_key_exists('enabled', $config)) { + unset($config['enabled']); + } + + $container->setParameter('chill_person.person_fields', $config); + + foreach ($config as $key => $value) { + $container->setParameter('chill_person.person_fields.'.$key, $value); + } + } private function declarePersonAsCustomizable (ContainerBuilder $container) { @@ -67,6 +82,18 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $asseticConfig['bundles'][] = 'ChillPersonBundle'; $container->prependExtensionConfig('assetic', array('bundles' => array('ChillPersonBundle'))); + + //add person_fields parameter as global + $chillPersonConfig = $container->getExtensionConfig($this->getAlias()); + $config = $this->processConfiguration(new Configuration(), $chillPersonConfig); + $twigConfig = array( + 'globals' => array( + 'chill_person' => array( + 'fields' => $config['person_fields'] + ) + ) + ); + $container->prependExtensionConfig('twig', $twigConfig); $this-> declarePersonAsCustomizable($container); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 833b5498d..8a935b8b1 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -27,40 +27,71 @@ class Configuration implements ConfigurationInterface $rootNode ->canBeDisabled() ->children() - ->arrayNode('search') - ->canBeDisabled() + ->arrayNode('search') + ->canBeDisabled() ->children() - ->booleanNode('use_double_metaphone') - ->defaultFalse() - ->end() - ->booleanNode('use_trigrams') - ->defaultFalse() - ->end() - ->end() - ->end() - ->arrayNode('validation') - ->canBeDisabled() - ->children() - ->scalarNode('birthdate_not_after') - ->info($this->validationBirthdateNotAfterInfos) - ->defaultValue('P1D') - ->validate() - ->ifTrue(function($period) { - try { - $interval = new \DateInterval($period); - } catch (\Exception $ex) { - return true; - } - return false; - }) - ->thenInvalid('Invalid period for birthdate validation : "%s" ' - . 'The parameter should match duration as defined by ISO8601 : ' - . 'https://en.wikipedia.org/wiki/ISO_8601#Durations') - ->end() - ->end() - ->end(); + ->booleanNode('use_double_metaphone') + ->defaultFalse() + ->end() // use_double_metaphone, parent = children for 'search' + ->booleanNode('use_trigrams') + ->defaultFalse() + ->end() // use_trigrams, parent = children of 'search' + ->end() //children for 'search', parent = array node 'search' + ->end() // array 'search', parent = children of root + ->arrayNode('validation') + ->canBeDisabled() + ->children() + ->scalarNode('birthdate_not_after') + ->info($this->validationBirthdateNotAfterInfos) + ->defaultValue('P1D') + ->validate() + ->ifTrue(function($period) { + try { + $interval = new \DateInterval($period); + } catch (\Exception $ex) { + return true; + } + return false; + }) + ->thenInvalid('Invalid period for birthdate validation : "%s" ' + . 'The parameter should match duration as defined by ISO8601 : ' + . 'https://en.wikipedia.org/wiki/ISO_8601#Durations') + ->end() // birthdate_not_after, parent = children of validation + + ->end() // children for 'validation', parent = validation + ->end() //validation, parent = children of root + ->end() // children of root, parent = root + ->arrayNode('person_fields') + ->canBeDisabled() + ->children() + ->append($this->addFieldNode('place_of_birth')) + ->append($this->addFieldNode('email')) + ->append($this->addFieldNode('phonenumber')) + ->append($this->addFieldNode('nationality')) + ->append($this->addFieldNode('country_of_birth')) + ->append($this->addFieldNode('marital_status')) + ->append($this->addFieldNode('spoken_languages')) + ->append($this->addFieldNode('address')) + ->end() //children for 'person_fields', parent = array 'person_fields' + ->end() // person_fields, parent = children of root + ->end() // children of 'root', parent = root + ; return $treeBuilder; } + + private function addFieldNode($key) + { + $tree = new TreeBuilder(); + $node = $tree->root($key, 'enum'); + + $node + ->values(array('hidden', 'visible')) + ->defaultValue('visible') + ->info("If the field $key must be shown") + ->end(); + //var_dump($node); + return $node; + } } diff --git a/Entity/Person.php b/Entity/Person.php index 9f707790b..b357fafb2 100644 --- a/Entity/Person.php +++ b/Entity/Person.php @@ -27,6 +27,8 @@ use Chill\MainBundle\Entity\Country; use Chill\PersonBundle\Entity\MaritalStatus; use Doctrine\Common\Collections\ArrayCollection; use Chill\MainBundle\Entity\HasCenterInterface; +use Chill\MainBundle\Entity\Address; +use Doctrine\Common\Collections\Criteria; /** * Person @@ -100,9 +102,16 @@ class Person implements HasCenterInterface { /** @var array Array where customfield's data are stored */ private $cFData; + /** + * + * @var \Doctrine\Common\Collections\Collection + */ + private $addresses; + public function __construct(\DateTime $opening = null) { $this->accompanyingPeriods = new ArrayCollection(); $this->spokenLanguages = new ArrayCollection(); + $this->addresses = new ArrayCollection(); if ($opening === null) { $opening = new \DateTime(); @@ -597,6 +606,45 @@ class Person implements HasCenterInterface { return $this->spokenLanguages; } + public function addAddress(Address $address) + { + $this->addresses[] = $address; + + return $this; + } + + public function removeAddress(Address $address) + { + $this->addresses->removeElement($address); + } + + /** + * By default, the addresses are ordered by date, descending (the most + * recent first) + * + * @return \Chill\MainBundle\Entity\Address[] + */ + public function getAddresses() + { + return $this->addresses; + } + + public function getLastAddress(\DateTime $date = null) + { + if ($date === null) { + $date = new \DateTime('now'); + } + + $addresses = $this->getAddresses(); + + if ($addresses == null) { + + return null; + } + + return $addresses->first(); + } + /** * Validation callback that checks if the accompanying periods are valid * diff --git a/Form/PersonType.php b/Form/PersonType.php index 16f50cf6f..aa76a3ba7 100644 --- a/Form/PersonType.php +++ b/Form/PersonType.php @@ -28,6 +28,25 @@ use Chill\PersonBundle\Form\Type\GenderType; class PersonType extends AbstractType { + /** + * array of configuration for person_fields. + * + * Contains whether we should add fields some optional fields (optional per + * instance) + * + * @var string[] + */ + protected $config = array(); + + /** + * + * @param string[] $personFieldsConfiguration configuration of visibility of some fields + */ + public function __construct(array $personFieldsConfiguration) + { + $this->config = $personFieldsConfiguration; + } + /** * @param FormBuilderInterface $builder * @param array $options @@ -38,27 +57,48 @@ class PersonType extends AbstractType ->add('firstName') ->add('lastName') ->add('birthdate', 'date', array('required' => false, 'widget' => 'single_text', 'format' => 'dd-MM-yyyy')) - ->add('placeOfBirth', 'text', array('required' => false)) ->add('gender', new GenderType(), array( 'required' => true )) ->add('memo', 'textarea', array('required' => false)) - ->add('phonenumber', 'textarea', array('required' => false)) - ->add('email', 'textarea', array('required' => false)) - ->add('countryOfBirth', 'select2_chill_country', array( + ; + + if ($this->config['place_of_birth'] === 'visible') { + $builder->add('placeOfBirth', 'text', array('required' => false)); + } + + if ($this->config['phonenumber'] === 'visible') { + $builder->add('phonenumber', 'textarea', array('required' => false)); + } + + if ($this->config['email'] === 'visible') { + $builder->add('email', 'textarea', array('required' => false)); + } + + if ($this->config['country_of_birth'] === 'visible') { + $builder->add('countryOfBirth', 'select2_chill_country', array( 'required' => false - )) - ->add('nationality', 'select2_chill_country', array( + )); + } + + if ($this->config['nationality'] === 'visible') { + $builder->add('nationality', 'select2_chill_country', array( 'required' => false - )) - ->add('spokenLanguages', 'select2_chill_language', array( + )); + } + + if ($this->config['spoken_languages'] === 'visible') { + $builder->add('spokenLanguages', 'select2_chill_language', array( 'required' => false, 'multiple' => true - )) - ->add('maritalStatus', 'select2_chill_marital_status', array( + )); + } + + if ($this->config['marital_status'] === 'visible'){ + $builder->add('maritalStatus', 'select2_chill_marital_status', array( 'required' => false - )) - ; + )); + } if($options['cFGroup']) { $builder diff --git a/Form/Type/PickPersonType.php b/Form/Type/PickPersonType.php new file mode 100644 index 000000000..b9932c4ed --- /dev/null +++ b/Form/Type/PickPersonType.php @@ -0,0 +1,161 @@ + + * + * 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\PersonBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Chill\MainBundle\Entity\Center; +use Chill\PersonBundle\Entity\PersonRepository; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Entity\GroupCenter; +use Chill\PersonBundle\Entity\Person; + +/** + * This type allow to pick a person. + * + * The form is embedded in a select2 input. + * + * The people may be filtered : + * + * - with the `centers` option, only the people associated with the given center(s) + * are seen. May be an instance of `Chill\MainBundle\Entity\Center`, or an array of + * `Chill\MainBundle\Entity\Center`. By default, all the reachable centers as selected. + * - with the `role` option, only the people belonging to the reachable center for the + * given role are displayed. + * + * + * @author Julien Fastré + */ +class PickPersonType extends AbstractType +{ + /** + * @var PersonRepository + */ + protected $personRepository; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + public function __construct( + PersonRepository $personRepository, + TokenStorageInterface $tokenStorage, + AuthorizationHelper $authorizationHelper + ) + { + $this->personRepository = $personRepository; + $this->user = $tokenStorage->getToken()->getUser(); + $this->authorizationHelper = $authorizationHelper; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $qb = $options['query_builder']; + + if ($options['role'] === NULL) { + $centers = array_map(function (GroupCenter $g) { + + return $g->getCenter(); + }, $this->user->getGroupCenters()->toArray()); + } else { + $centers = $this->authorizationHelper + ->getReachableCenters($this->user, $options['role']); + } + + if ($options['centers'] === NULL) { + // we select all selected centers + $selectedCenters = $centers; + } else { + $selectedCenters = array(); + $options['centers'] = is_array($options['centers']) ? + $options['centers'] : array($options['centers']); + + foreach ($options['centers'] as $c) { + // check that every member of the array is a center + if (!$c instanceof Center) { + throw new \RuntimeException('Every member of the "centers" ' + . 'option must be an instance of '.Center::class); + } + if (!in_array($c->getId(), array_map( + function(Center $c) { return $c->getId();}, + $centers))) { + throw new AccessDeniedException('The given center is not reachable'); + } + $selectedCenters[] = $c; + } + } + + + $qb + ->orderBy('p.firstName', 'ASC') + ->orderBy('p.lastName', 'ASC') + ->where($qb->expr()->in('p.center', ':centers')) + ->setParameter('centers', $selectedCenters) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + // add the possibles options for this type + $resolver->setDefined('centers') + ->addAllowedTypes('centers', array('array', Center::class, 'null')) + ->setDefault('centers', null) + ->setDefined('role') + ->addAllowedTypes('role', array(Role::class, 'null')) + ->setDefault('role', null) + ; + + // add the default options + $resolver->setDefaults(array( + 'class' => Person::class, + 'choice_label' => function(Person $p) { + return $p->getFirstname().' '.$p->getLastname(); + }, + 'placeholder' => 'Pick a person', + 'choice_attr' => function(Person $p) { + return array( + 'data-center' => $p->getCenter()->getId() + ); + }, + 'attr' => array('class' => 'select2 '), + 'query_builder' => $this->personRepository->createQueryBuilder('p') + )); + } + + public function getParent() + { + return \Symfony\Bridge\Doctrine\Form\Type\EntityType::class; + } + +} diff --git a/Resources/config/doctrine/Person.orm.yml b/Resources/config/doctrine/Person.orm.yml index 838ab1e8f..e566ddc7e 100644 --- a/Resources/config/doctrine/Person.orm.yml +++ b/Resources/config/doctrine/Person.orm.yml @@ -72,4 +72,10 @@ Chill\PersonBundle\Entity\Person: inverseJoinColumns: language_id: referencedColumnName: id + addresses: + targetEntity: Chill\MainBundle\Entity\Address + orderBy: { 'validFrom': 'DESC' } + joinTable: + name: chill_person_persons_to_addresses + cascade: [persist, remove, merge, detach] lifecycleCallbacks: { } diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 91b481ea4..927eb59e5 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -69,6 +69,27 @@ chill_person_accompanying_period_close: chill_person_accompanying_period_open: path: /{_locale}/person/{person_id}/accompanying-period/open defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:open } + +chill_person_address_list: + path: /{_locale}/person/{person_id}/address/list + defaults: { _controller: ChillPersonBundle:PersonAddress:list } + +chill_person_address_create: + path: /{_locale}/person/{person_id}/address/create + defaults: { _controller: ChillPersonBundle:PersonAddress:create } + methods: [POST] + +chill_person_address_new: + path: /{_locale}/person/{person_id}/address/new + defaults: { _controller: ChillPersonBundle:PersonAddress:new } + +chill_person_address_edit: + path: /{_locale}/person/{person_id}/address/{address_id}/edit + defaults: { _controller: ChillPersonBundle:PersonAddress:edit } + +chill_person_address_update: + path: /{_locale}/person/{person_id}/address/{address_id}/update + defaults: { _controller: ChillPersonBundle:PersonAddress:update } chill_person_export: path: /{_locale}/person/export/ diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 0e15965de..819f3e46f 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -1,7 +1,14 @@ parameters: # cl_chill_person.example.class: Chill\PersonBundle\Example -services: +services: + chill.person.form.person_creation: + class: Chill\PersonBundle\Form\PersonType + arguments: + - %chill_person.person_fields% + tags: + - { name: form.type } + chill.person.accompanying_period_closing_motive: class: Chill\PersonBundle\Form\Type\ClosingMotiveType scope: request @@ -93,4 +100,19 @@ services: arguments: - "@translator" tags: - - { name: chill.export_aggregator, alias: person_gender_aggregator } \ No newline at end of file + - { name: chill.export_aggregator, alias: person_gender_aggregator } + + chill.person.form.type.pick_person: + class: Chill\PersonBundle\Form\Type\PickPersonType + arguments: + - "@chill.person.repository.person" + - "@security.token_storage" + - "@chill.main.security.authorization.helper" + tags: + - { name: form.type } + + chill.person.repository.person: + class: Chill\PersonBundle\Entity\PersonRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\PersonBundle\Entity\Person' diff --git a/Resources/migrations/Version20160310161006.php b/Resources/migrations/Version20160310161006.php new file mode 100644 index 000000000..f17ae910c --- /dev/null +++ b/Resources/migrations/Version20160310161006.php @@ -0,0 +1,49 @@ +abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE TABLE chill_person_persons_to_addresses (' + . 'person_id INT NOT NULL, ' + . 'address_id INT NOT NULL, ' + . 'PRIMARY KEY(person_id, address_id))'); + $this->addSql('CREATE INDEX IDX_4655A196217BBB47 ' + . 'ON chill_person_persons_to_addresses (person_id)'); + $this->addSql('CREATE INDEX IDX_4655A196F5B7AF75 ' + . 'ON chill_person_persons_to_addresses (address_id)'); + $this->addSql('ALTER TABLE chill_person_persons_to_addresses ' + . 'ADD CONSTRAINT FK_4655A196217BBB47 ' + . 'FOREIGN KEY (person_id) ' + . 'REFERENCES Person (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_persons_to_addresses ' + . 'ADD CONSTRAINT FK_4655A196F5B7AF75 ' + . 'FOREIGN KEY (address_id) ' + . 'REFERENCES chill_main_address (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP TABLE chill_person_persons_to_addresses'); + + } +} diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index f5022bd77..8836cb641 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -68,8 +68,24 @@ Reset: 'Remise à zéro' 'Create accompanying period': 'Nouvelle ouverture-fermeture à une autre date' 'Closing motive': 'Motif de clôture' 'Person details': 'Détails de la personne' +'Update details for %name%': 'Modifier détails de %name%' Accompanying period list: Périodes d'accompagnement +# pickAPersonType +Pick a person: Choisir une personne + +#address +Since %date%: Depuis le %date% +No address given: Pas d'adresse renseignée +The address has been successfully updated: L'adresse a été mise à jour avec succès +Update address for %name%: Mettre à jour une adresse pour %name% +Addresses'history for %name%: Historique des adresses de %name% +Addresses'history: Historique des adresses +New address for %name% : Nouvelle adresse pour %name% +The new address was created successfully: La nouvelle adresse a été créée +Add an address: Ajouter une adresse +Back to the person details: Retour aux détails de la personne + #timeline 'An accompanying period is opened for %person% on %date%': Une période d'accompagnement a été ouverte le %date% pour %person% 'An accompanying period is closed for %person% on %date%': Une période d'accompagnement a été fermée le %date% pour %person% diff --git a/Resources/views/Address/edit.html.twig b/Resources/views/Address/edit.html.twig new file mode 100644 index 000000000..8090416aa --- /dev/null +++ b/Resources/views/Address/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 "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %} + +{% block personcontent %} + +

{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

+ + {{ form_start(form) }} + + {{ form_row(form.streetAddress1) }} + {{ form_row(form.streetAddress2) }} + {{ form_row(form.postCode) }} + {{ form_row(form.validFrom) }} + + + + {{ form_end(form) }} + +{% endblock personcontent %} \ No newline at end of file diff --git a/Resources/views/Address/list.html.twig b/Resources/views/Address/list.html.twig new file mode 100644 index 000000000..cc382514f --- /dev/null +++ b/Resources/views/Address/list.html.twig @@ -0,0 +1,84 @@ +{# + * 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 "ChillPersonBundle::layout.html.twig" %} + +{% import 'ChillMainBundle:Address:macro.html.twig' as address_macros %} + +{% set activeRouteKey = '' %} + +{% block title %}{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}{% endblock %} + +{% block personcontent %} + +

{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

+ + + + + + + + + + + {% if person.addresses|length == 0 %} + + + + {% else %} + {% for address in person.addresses %} + + + + + + + + {% endfor %} + {% endif %} + +
{{ 'Valid from'|trans }}{{ 'Address'|trans }} 
+ {{ 'No address given'|trans }} + + {{ 'Add an address'|trans }} + +
{{ 'Since %date%'|trans( { '%date%' : address.validFrom|localizeddate('long', 'none') } ) }} + {{ address_macros._render(address, { 'with_valid_from' : false } ) }} + + +
+ + + +{% endblock personcontent %} \ No newline at end of file diff --git a/Resources/views/Address/new.html.twig b/Resources/views/Address/new.html.twig new file mode 100644 index 000000000..8e9f9a4ca --- /dev/null +++ b/Resources/views/Address/new.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 "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% block title %}{{ 'New address for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %} + +{% block personcontent %} + +

{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

+ + {{ form_start(form) }} + + {{ form_row(form.streetAddress1) }} + {{ form_row(form.streetAddress2) }} + {{ form_row(form.postCode) }} + {{ form_row(form.validFrom) }} + + + + {{ form_end(form) }} + +{% endblock personcontent %} \ No newline at end of file diff --git a/Resources/views/Person/edit.html.twig b/Resources/views/Person/edit.html.twig index 5b768f37c..89bc4b44c 100644 --- a/Resources/views/Person/edit.html.twig +++ b/Resources/views/Person/edit.html.twig @@ -18,14 +18,21 @@ {% set activeRouteKey = '' %} -{% block title %}ChillPersonBundle:Person:see{% endblock %} +{% block title %}{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %} {% block personcontent %} + +

{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName|capitalize } ) }}

{% form_theme form 'ChillMainBundle:Form:fields.html.twig' %} {{ form_start(form) }} +
+

{{ 'Memo'|trans }}

+{{ form_row(form.memo, {'label' : 'Memo'} ) }} +
+

{{ 'General information'|trans }}

{{ form_row(form.firstName, {'label' : 'First name'}) }} @@ -36,25 +43,41 @@

{{ 'Birth information'|trans }}

{{ form_row(form.birthdate, {'label': 'Date of birth'} ) }} + {%- if form.placeOfBirth is defined -%} {{ form_row(form.placeOfBirth, { 'label' : 'Place of birth'} ) }} + {%- endif -%} + {%- if form.countryOfBirth is defined -%} {{ form_row(form.countryOfBirth, { 'label' : 'Country of birth' } ) }} + {%- endif -%}
+{%- if form.nationality is defined or form.spokenLanguages is defined or form.maritalStatus is defined -%}

{{ 'Administrative information'|trans }}

+ {%- if form.nationality is defined -%} {{ form_row(form.nationality, { 'label' : 'Nationality'|trans} ) }} + {%- endif -%} + {%- if form.spokenLanguages is defined -%} {{ form_row(form.spokenLanguages, {'label' : 'Spoken languages'}) }} + {%- endif -%} + {%- if form.maritalStatus is defined -%} {{ form_row(form.maritalStatus, { 'label' : 'Marital status'} ) }} + {%- endif -%}
+{%- endif -%} +{%- if form.email is defined or form.phonenumber is defined -%}

{{ 'Contact information'|trans }}

+ {%- if form.email is defined -%} {{ form_row(form.email, {'label': 'Email'}) }} + {%- endif -%} + {%- if form.phonenumber is defined -%} {{ form_row(form.phonenumber, {'label': 'Phonenumber'}) }} - + {%- endif -%}
+{%- endif -%} -{{ form_row(form.memo, {'label' : 'Memo'} ) }} {{ form_rest(form) }} diff --git a/Resources/views/Person/macro.html.twig b/Resources/views/Person/macro.html.twig new file mode 100644 index 000000000..e6773f3ae --- /dev/null +++ b/Resources/views/Person/macro.html.twig @@ -0,0 +1 @@ +{% macro render(p, withLink=false) %}{{ p.firstName }} {{ p.lastName }}{% endmacro %} \ No newline at end of file diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index 7f93df1ff..7290aedcc 100644 --- a/Resources/views/Person/view.html.twig +++ b/Resources/views/Person/view.html.twig @@ -16,6 +16,8 @@ #} {% extends "ChillPersonBundle::layout.html.twig" %} +{% import 'ChillMainBundle:Address:macro.html.twig' as address %} + {% set activeRouteKey = 'chill_person_view' %} {# @@ -23,7 +25,8 @@ This view should receive those arguments: - person #} -{% block title %}ChillPersonBundle:Person:see{% endblock %} +{% block title %}{{ 'Person details'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~ + ' ' ~ person.lastName }}{% endblock %} {# we define variables to include an edit form repeated multiple time across the page @@ -34,8 +37,21 @@ This view should receive those arguments: {% block personcontent %} + +
+ {% if person.memo is not empty %} +
+
+

{{ 'Memo'|trans|upper }}

+ +

+

{{ person.memo|nl2br }}
+

+
+
+ {% endif %}

{{ 'General information'|trans|upper }}

@@ -72,8 +88,12 @@ This view should receive those arguments: {%- endif -%} + {%- if chill_person.fields.place_of_birth == 'visible' -%}
{{ 'Place of birth'|trans }} :
{{ person.placeOfBirth }}
+ {%- endif -%} + {%- if chill_person.fields.country_of_birth == 'visible' -%} +
{{ 'Country of birth'|trans }} :
{% spaceless %} {% if person.countryOfBirth is not null %} {{ person.countryOfBirth.name|localize_translatable_string }} @@ -81,6 +101,7 @@ This view should receive those arguments: {{ 'Unknown country of birth'|trans }} {% endif %} {% endspaceless %}
+ {%- endif -%} {% if is_granted('CHILL_PERSON_UPDATE', person) %} @@ -91,10 +112,12 @@ This view should receive those arguments:
+ {%- if chill_person.fields.nationality == 'visible' or chill_person.fields.spoken_languages == 'visible'-%}

{{ 'Administrative information'|trans|upper }}

+ {%- if chill_person.fields.nationality == 'visible' -%}
{{ 'Nationality'|trans }} :
@@ -105,6 +128,8 @@ This view should receive those arguments: {% endif %}
+ {%- endif -%} + {%- if chill_person.fields.spoken_languages == 'visible' -%}
{{'Spoken languages'|trans}} :
@@ -117,6 +142,8 @@ This view should receive those arguments: {% endif %}
+ {%- endif -%} + {%- if chill_person.fields.marital_status == 'visible' -%}
{{'Marital status'|trans}} :
@@ -127,24 +154,53 @@ This view should receive those arguments: {% endif %}
+ {%- endif -%} {% if is_granted('CHILL_PERSON_UPDATE', person) %} {{ include(edit_tmp_name, edit_tmp_args) }} {% endif %}
+ {%- endif -%} + {%- if chill_person.fields.email == 'visible' or chill_person.fields.phonenumber == 'visible' -%}

 {{ 'Contact information'|trans|upper }}

+ {%- if chill_person.fields.address == 'visible' -%} +
+
{{ 'Address'|trans }}
+
+ {%- if person.lastAddress is not empty -%} + {{ address._render(person.lastAddress) }} + + {{ 'Edit'|trans }} +
+ + {{ 'Addresses\'history'|trans }} + + {%- else -%} + {{ 'No address given'|trans }} + + {{ 'Add an address'|trans }} + + {%- endif -%} +
+
+ {%- endif -%} + + {%- if chill_person.fields.email == 'visible' -%}
{{ 'Email'|trans }} :
{{ person.email}} 
+ {%- endif -%} + {%- if chill_person.fields.phonenumber == 'visible' -%}
{{ 'Phonenumber'|trans }} :
{{ person.phonenumber}} 
+ {% endif %} {% if is_granted('CHILL_PERSON_UPDATE', person) %} @@ -152,6 +208,7 @@ This view should receive those arguments: {% endif %}
+ {%- endif -%}
{% if cFGroup and (cFGroup.getActiveCustomFields|length > 0) %} diff --git a/Resources/views/layout.html.twig b/Resources/views/layout.html.twig index 78a674a12..3a9b6aae5 100644 --- a/Resources/views/layout.html.twig +++ b/Resources/views/layout.html.twig @@ -70,6 +70,7 @@ {{ person.birthdate|localizeddate('long', 'none') }} {% endif %}
+ {%- if chill_person.fields.nationality == 'visible' -%}
{{ 'Nationality'|trans|upper}} : {% if person.nationality is not null %} @@ -78,6 +79,8 @@ {% trans %}Without nationality{% endtrans %} {% endif %}
+ {%- endif -%} + {%- if chill_person.fields.spoken_languages == 'visible' -%}
{{ 'Spoken languages'|trans|upper}} : {% if person.spokenLanguages|length == 0 %} @@ -87,7 +90,8 @@ {{ lang.name|localize_translatable_string }}{% if not loop.last %},{% endif %} {% endfor %} {% endif %} -
+ + {%- endif -%} {% endblock %} @@ -104,4 +108,8 @@ 'args' : {'person_id': person.id }, 'activeRouteKey': activeRouteKey }) }} + +
+ {{ chill_delegated_block('person_post_vertical_menu', { 'person': person } ) }} +
{% endblock %} \ No newline at end of file diff --git a/Tests/Controller/PersonAddressControllerTest.php b/Tests/Controller/PersonAddressControllerTest.php new file mode 100644 index 000000000..0f23236a8 --- /dev/null +++ b/Tests/Controller/PersonAddressControllerTest.php @@ -0,0 +1,189 @@ + + * + * 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\PersonBundle\Tests\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Chill\PersonBundle\Entity\Person; + +/** + * + * + * @author Julien Fastré + */ +class PersonAddressControllerTest extends WebTestCase +{ + /** @var \Doctrine\ORM\EntityManagerInterface The entity manager */ + protected $em; + + /** @var Person The person on which the test is executed */ + protected static $person; + + /** + * + * @var \Chill\MainBundle\Entity\PostalCode + */ + protected $postalCode; + + /** + * + * @var \Symfony\Component\BrowserKit\Client + */ + protected $client; + + public static function setUpBeforeClass() + { + static::bootKernel(); + + $em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = $em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + + self::$person = (new Person()) + ->setLastName("Tested person") + ->setFirstName("Test") + ->setCenter($center) + ->setGender(Person::MALE_GENDER); + + $em->persist(self::$person); + $em->flush(); + } + + /** + * Prepare client and create a random person + */ + public function setUp() + { + static::bootKernel(); + + $this->em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $this->postalCode = $this->em->getRepository('ChillMainBundle:PostalCode') + ->findOneBy(array('code' => 1000)); + + $this->client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_PW' => 'password', + )); + } + + public static function tearDownAfter() + { + $this->refreshPerson(); + $this->em->remove(self::$person); + $this->em->flush(); + } + + /** + * Reload the person from the db + */ + protected function refreshPerson() + { + self::$person = $this->em->getRepository('ChillPersonBundle:Person') + ->find(self::$person->getId()); + } + + public function testEmptyList() + { + $crawler = $this->client->request('GET', '/fr/person/'. + self::$person->getId().'/address/list'); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + + $this->assertEquals(1, $crawler->filter('td:contains("Pas d\'adresse renseignée")') + ->count(), + "assert that a message say 'no address given'"); + + } + + /** + * @depends testEmptyList + */ + public function testCreateAddress() + { + $crawler = $this->client->request('GET', '/fr/person/'. + self::$person->getId().'/address/new'); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + + $form = $crawler->selectButton('Envoi')->form(array( + 'address[streetAddress1]' => 'Rue de la Paix, 50', + 'address[streetAddress2]' => $this->postalCode->getId(), + 'address[validFrom]' => '15-01-2016' + )); + + $this->client->submit($form); + + $crawler = $this->client->followRedirect(); + + $this->assertRegexp('|/fr/person/[0-9]{1,}/address/list|', + $this->client->getHistory()->current()->getUri(), + "assert that the current page is on |/fr/person/[0-9]{1,}/address/list|"); + $this->assertEquals(1, $crawler + ->filter('div.flash_message.success') + ->count(), + "Asserting that the response page contains a success flash message"); + $this->assertEquals(1, $crawler + ->filter('td:contains("Rue de la Paix, 50")') + ->count(), + "Asserting that the page contains the new address"); + + } + + /** + * @depends testCreateAddress + */ + public function testUpdateAddress() + { + $this->refreshPerson(); + $address = self::$person->getLastAddress(); + + $crawler = $this->client->request('GET', '/fr/person/'.self::$person->getId() + .'/address/'.$address->getId().'/edit'); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + + $form = $crawler->selectButton('Envoi')->form(array( + 'address[streetAddress1]' => 'Rue du Trou Normand, 15', + 'address[validFrom]' => '15-01-2015' + )); + + $this->client->submit($form); + + $crawler = $this->client->followRedirect(); + + $this->assertRegexp('|/fr/person/[0-9]{1,}/address/list|', + $this->client->getHistory()->current()->getUri(), + "assert that the current page is on |/fr/person/[0-9]{1,}/address/list|"); + $this->assertGreaterThan(0, $crawler + ->filter('div.flash_message.success') + ->count(), + "Asserting that the response page contains a success flash message"); + $this->assertEquals(1, $crawler + ->filter('td:contains("Rue du Trou Normand")') + ->count(), + "Asserting that the page contains the new address"); + } + + + +} diff --git a/Tests/Controller/PersonControllerCreateTest.php b/Tests/Controller/PersonControllerCreateTest.php index 83acbbf7b..5f0a33fab 100644 --- a/Tests/Controller/PersonControllerCreateTest.php +++ b/Tests/Controller/PersonControllerCreateTest.php @@ -240,6 +240,31 @@ class PersonControllerCreateTest extends WebTestCase } + public function testReviewExistingDetectionInversedLastNameWithFirstName() + { + $client = $this->getAuthenticatedClient(); + + $crawler = $client->request('GET', '/fr/person/new'); + + //test the page is loaded before continuing + $this->assertTrue($client->getResponse()->isSuccessful()); + + $form = $crawler->selectButton("Ajouter la personne")->form(); + $form = $this->fillAValidCreationForm($form, 'Charline', 'dd'); + $client->submit($form); + + $this->assertContains('Depardieu', $client->getCrawler()->text(), + "check that the page has detected the lastname of a person existing in database"); + + //inversion + $form = $crawler->selectButton("Ajouter la personne")->form(); + $form = $this->fillAValidCreationForm($form, 'dd', 'Charline'); + $client->submit($form); + + $this->assertContains('Depardieu', $client->getCrawler()->text(), + "check that the page has detected the lastname of a person existing in database"); + } + public static function tearDownAfterClass() { static::bootKernel(); diff --git a/Tests/Controller/PersonControllerUpdateTest.php b/Tests/Controller/PersonControllerUpdateTest.php index 9429e4e3e..b7087024d 100644 --- a/Tests/Controller/PersonControllerUpdateTest.php +++ b/Tests/Controller/PersonControllerUpdateTest.php @@ -96,6 +96,24 @@ class PersonControllerUpdateTest extends WebTestCase "The person edit form is accessible"); } + /** + * Test the configurable fields are present + * + * @group configurable_fields + */ + public function testHiddenFielsArePresent() + { + $crawler = $this->client->request('GET', $this->editUrl); + + $configurables = array('placeOfBirth', 'phonenumber', 'email', + 'countryOfBirth', 'nationality', 'spokenLanguages', 'maritalStatus'); + $form = $crawler->selectButton('Submit')->form(); //; + + foreach($configurables as $key) { + $this->assertTrue($form->has('chill_personbundle_person['.$key.']')); + } + } + /** * Test if the edit page of a given person is not accessible for a user * of another center of the person diff --git a/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php b/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php new file mode 100644 index 000000000..7d374a3a7 --- /dev/null +++ b/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php @@ -0,0 +1,211 @@ + + * + * 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\PersonBundle\Tests\Controller; + +//ini_set('memory_limit', '-1'); + +use Chill\PersonBundle\Entity\Person; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * Test the edition of persons + * + * As I am logged in as "center a_social" + * + * @author Julien Fastré + */ +class PersonControllerUpdateWithHiddenFieldsTest extends WebTestCase +{ + /** @var \Doctrine\ORM\EntityManagerInterface The entity manager */ + private $em; + + /** @var Person The person on which the test is executed */ + private $person; + + /** @var string The url using for editing the person's information */ + private $editUrl; + + /** @var string The url using for seeing the person's information */ + private $viewUrl; + + /** + * Prepare client and create a random person + */ + public function setUp() + { + static::bootKernel(array('environment' => 'test_with_hidden_fields')); + + $this->em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = $this->em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + + $this->person = (new Person()) + ->setLastName("My Beloved") + ->setFirstName("Jesus") + ->setCenter($center) + ->setGender(Person::MALE_GENDER); + + $this->em->persist($this->person); + $this->em->flush(); + + $this->editUrl = '/en/person/'.$this->person->getId().'/general/edit'; + $this->viewUrl = '/en/person/'.$this->person->getId().'/general'; + + $this->client = static::createClient( + array( + 'environment' => 'test_with_hidden_fields' + ), + array( + 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_PW' => 'password', + ) + ); + } + + /** + * Reload the person from the db + */ + protected function refreshPerson() + { + $this->person = $this->em->getRepository('ChillPersonBundle:Person') + ->find($this->person->getId()); + } + + /** + * Test the edit page are accessible + */ + public function testEditPageIsSuccessful() + { + $this->client->request('GET', $this->editUrl); + $this->assertTrue($this->client->getResponse()->isSuccessful(), + "The person edit form is accessible"); + } + + /** + * Test the configurable fields are absent + * + * @group configurable_fields + */ + public function testHiddenFielsAreAbsent() + { + $crawler = $this->client->request('GET', $this->editUrl); + + $configurables = array('placeOfBirth', 'phonenumber', 'email', + 'countryOfBirth', 'nationality', 'spokenLanguages', 'maritalStatus'); + $form = $crawler->selectButton('Submit')->form(); //; + + foreach($configurables as $key) { + $this->assertFalse($form->has('chill_personbundle_person['.$key.']')); + } + } + + /** + * Test the edition of a field + * + * Given I fill the field with $value + * And I submit the form + * Then I am redirected to the 'general' page + * And the person is updated in the db + * + * @dataProvider validTextFieldsProvider + * @param string $field + * @param string $value + * @param \Closure $callback + */ + public function testEditTextField($field, $value, \Closure $callback) + { + $crawler = $this->client->request('GET', $this->editUrl); + + $form = $crawler->selectButton('Submit') + ->form(); + //transform countries into value if needed + switch ($field) { + case 'nationality': + case 'countryOfBirth': + if ($value !== NULL) { + $country = $this->em->getRepository('ChillMainBundle:Country') + ->findOneByCountryCode($value); + $transformedValue = $country->getId(); + } else { + $transformedValue = NULL; + } + break; + default: + $transformedValue = $value; + } + + $form->get('chill_personbundle_person['.$field. ']') + ->setValue($transformedValue); + + $this->client->submit($form); + $this->refreshPerson(); + + $this->assertTrue($this->client->getResponse()->isRedirect($this->viewUrl), + 'the page is redirected to general view'); + $this->assertEquals($value, $callback($this->person), + 'the value '.$field.' is updated in db'); + + $crawler = $this->client->followRedirect(); + $this->assertGreaterThan(0, $crawler->filter('.success')->count(), + 'a element .success is shown'); + + if($field == 'birthdate' or $field == 'memo' or $field == 'countryOfBirth' or $field == 'nationality' + or $field == 'gender') { + // we do not perform test on the web page contents. + } else { + $this->assertGreaterThan(0, $crawler->filter('html:contains("'.$value.'")')->count()); + } + } + + /** + * provide valid values to test, with field name and + * a function to find the value back from person entity + * + * @return mixed[] + */ + public function validTextFieldsProvider() + { + return array( + ['firstName', 'random Value', function(Person $person) { return $person->getFirstName(); } ], + ['lastName' , 'random Value', function(Person $person) { return $person->getLastName(); } ], + ['birthdate', '15-12-1980', function(Person $person) { return $person->getBirthdate()->format('d-m-Y'); }], + ['memo', 'jfkdlmq jkfldmsq jkmfdsq', function(Person $person) { return $person->getMemo(); }], + ['birthdate', '', function(Person $person) { return $person->getBirthdate(); }], + ['gender', Person::FEMALE_GENDER, function(Person $person) { return $person->getGender(); }], + ); + } + + public function tearDown() + { + $this->refreshPerson(); + $this->em->remove($this->person); + $this->em->flush(); + } + + private function getVeryLongText() + { + return <<assertGreaterThan(0, $crawler->filter('html:contains("Tested Person")')->count()); $this->assertGreaterThan(0, $crawler->filter('html:contains("Réginald")')->count()); + $this->assertContains('Email addresses', $crawler->text()); + $this->assertContains('Phonenumber', $crawler->text()); + $this->assertContains('Langues parlées', $crawler->text()); + $this->assertContains(/* Etat */ 'civil', $crawler->text()); } /** diff --git a/Tests/Controller/PersonControllerViewWithHiddenFieldsTest.php b/Tests/Controller/PersonControllerViewWithHiddenFieldsTest.php new file mode 100644 index 000000000..2ace5c1da --- /dev/null +++ b/Tests/Controller/PersonControllerViewWithHiddenFieldsTest.php @@ -0,0 +1,107 @@ + + * + * 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\PersonBundle\Tests\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Chill\PersonBundle\Entity\Person; + +/** + * @author Julien Fastré + * @author Marc Ducobu + */ +class PersonControllerViewTestWithHiddenFields extends WebTestCase +{ + /** @var \Doctrine\ORM\EntityManagerInterface The entity manager */ + private $em; + + /** @var Person A person used on which to run the test */ + private $person; + + /** @var String The url to view the person details */ + private $viewUrl; + + public function setUp() + { + static::bootKernel(array('environment' => 'test_with_hidden_fields')); + + $this->em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = $this->em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + + $this->person = (new Person()) + ->setLastName("Tested Person") + ->setFirstName("Réginald") + ->setCenter($center) + ->setGender(Person::MALE_GENDER); + + $this->em->persist($this->person); + $this->em->flush(); + + $this->viewUrl = '/en/person/'.$this->person->getId().'/general'; + } + + /** + * Test if the view page is accessible + * + * @group configurable_fields + */ + public function testViewPerson() + { + $client = static::createClient( + array('environment' => 'test_with_hidden_fields'), + array( + 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_PW' => 'password', + 'HTTP_ACCEPT_LANGUAGE' => 'fr' + ) + ); + + $crawler = $client->request('GET', $this->viewUrl); + $response = $client->getResponse(); + + $this->assertTrue($response->isSuccessful()); + + $this->assertGreaterThan(0, $crawler->filter('html:contains("Tested Person")')->count()); + $this->assertGreaterThan(0, $crawler->filter('html:contains("Réginald")')->count()); + $this->assertNotContains('Email addresses', $crawler->text()); + $this->assertNotContains('Phonenumber', $crawler->text()); + $this->assertNotContains('Langues parlées', $crawler->text()); + $this->assertNotContains(/* Etat */ 'civil', $crawler->text()); + } + + /** + * Reload the person from the db + */ + protected function refreshPerson() + { + $this->person = $this->em->getRepository('ChillPersonBundle:Person') + ->find($this->person->getId()); + } + + public function tearDown() + { + $this->refreshPerson(); + $this->em->remove($this->person); + $this->em->flush(); + } + +} diff --git a/Tests/Fixtures/App/app/config/config_test_with_hidden_fields.yml b/Tests/Fixtures/App/app/config/config_test_with_hidden_fields.yml new file mode 100644 index 000000000..e11599661 --- /dev/null +++ b/Tests/Fixtures/App/app/config/config_test_with_hidden_fields.yml @@ -0,0 +1,18 @@ +# config/config_test.yml +imports: + - { resource: config.yml } #here we import a config.yml file, this is not required + +framework: + test: ~ + session: + storage_id: session.storage.filesystem + +chill_person: + person_fields: + nationality: hidden + email: hidden + place_of_birth: hidden + phonenumber: hidden + country_of_birth: hidden + marital_status: hidden + spoken_languages: hidden \ No newline at end of file diff --git a/Tests/Form/Type/PickPersonTypeTest.php b/Tests/Form/Type/PickPersonTypeTest.php new file mode 100644 index 000000000..0baf96bfc --- /dev/null +++ b/Tests/Form/Type/PickPersonTypeTest.php @@ -0,0 +1,181 @@ + + * + * 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\PersonBundle\Tests\Form\Type; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Chill\PersonBundle\Form\Type\PickPersonType; + +/** + * + * + * @author Julien Fastré + */ +class PickPersonTypeTest extends KernelTestCase +{ + /** + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + /** + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * + * @var \Symfony\Component\Form\FormFactoryInterface + */ + protected $formFactory; + + public function setUp() + { + self::bootKernel(); + + $this->container = self::$kernel->getContainer(); + + $this->user = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:User') + ->findOneBy(array('username' => 'multi_center')); + + $this->formFactory = $this->container->get('form.factory'); + + $token = (new UsernamePasswordToken($this->user, 'password', 'firewall')); + $this->container->get('security.token_storage') + ->setToken($token); + } + + public function testWithoutOption() + { + $form = $this->formFactory + ->createBuilder(PickPersonType::class, null, array()) + ->getForm(); + + $this->assertInstanceOf(\Symfony\Component\Form\FormInterface::class, + $form); + + // transform into a view to have data-center attr + $view = $form->createView(); + + $centerIds = array(); + + /* @var $centerIds \Symfony\Component\Form\ChoiceList\View\ChoiceView */ + foreach($view->vars['choices'] as $choice) { + $centerIds[] = $choice->attr['data-center']; + } + + $this->assertEquals(2, count(array_unique($centerIds)), + "test that the form contains people from 2 centers"); + } + + /** + * Test the form with an option 'centers' with an unique center + * entity (not in an array) + */ + public function testWithOptionCenter() + { + $center = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')) + ; + + $form = $this->formFactory + ->createBuilder(PickPersonType::class, null, array( + 'centers' => $center + )) + ->getForm(); + + // transform into a view to have data-center attr + $view = $form->createView(); + + /* @var $centerIds \Symfony\Component\Form\ChoiceList\View\ChoiceView */ + foreach($view->vars['choices'] as $choice) { + $centerIds[] = $choice->attr['data-center']; + } + + $this->assertEquals(1, count(array_unique($centerIds)), + "test that the form contains people from only one centers"); + + $this->assertEquals($center->getId(), array_unique($centerIds)[0]); + + } + + /** + * Test the form with multiple centers + */ + public function testWithOptionCenters() + { + $centers = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:Center') + ->findAll() + ; + + $form = $this->formFactory + ->createBuilder(PickPersonType::class, null, array( + 'centers' => $centers + )) + ->getForm(); + + // transform into a view to have data-center attr + $view = $form->createView(); + + /* @var $centerIds \Symfony\Component\Form\ChoiceList\View\ChoiceView */ + foreach($view->vars['choices'] as $choice) { + $centerIds[] = $choice->attr['data-center']; + } + + $this->assertEquals(2, count(array_unique($centerIds)), + "test that the form contains people from only one centers"); + + } + + /** + * test with an invalid center type in the option 'centers' (in an array) + * + * @expectedException \RuntimeException + */ + public function testWithInvalidOptionCenters() + { + + $form = $this->formFactory + ->createBuilder(PickPersonType::class, null, array( + 'centers' => array('string') + )) + ->getForm(); + } + + public function testWithOptionRoleInvalid() + { + $form = $this->formFactory + ->createBuilder(PickPersonType::class, null, array( + 'role' => new \Symfony\Component\Security\Core\Role\Role('INVALID') + )) + ->getForm(); + + // transform into a view to have data-center attr + $view = $form->createView(); + + $this->assertEquals(0, count($view->vars['choices'])); + } + +} diff --git a/composer.json b/composer.json index 747218e89..d6448804f 100644 --- a/composer.json +++ b/composer.json @@ -16,13 +16,12 @@ } ], "require": { - "php": "~5.5", + "php": "~5.6|~7", "twig/extensions": "~1.0", "symfony/assetic-bundle": "~2.3", "symfony/monolog-bundle": "^2.7", - "symfony/framework-bundle": "~2.7", "symfony/yaml": "~2.7", - "symfony/symfony": "~2.7", + "symfony/symfony": "~2.8", "doctrine/dbal": "~2.5", "doctrine/orm": "~2.4", "doctrine/common": "~2.4", @@ -31,12 +30,10 @@ "chill-project/custom-fields": "dev-master@dev", "doctrine/doctrine-fixtures-bundle": "~2.2", "champs-libres/composer-bundle-migration": "~1.0", - "doctrine/doctrine-migrations-bundle": "dev-master@dev", + "doctrine/doctrine-migrations-bundle": "~1.1", "doctrine/migrations": "~1.0@dev" }, "require-dev": { - "symfony/dom-crawler": "2.5", - "symfony/security": "~2.5", "symfony/phpunit-bridge": "^2.7", "fzaninotto/faker": "~1" },