From afe6ace33187884fab7f3ff489e84a70cd7f5bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 10 Mar 2016 17:59:01 +0100 Subject: [PATCH 1/3] add a many-to-many relation to addresses --- DataFixtures/ORM/LoadPeople.php | 74 ++++++++++++++++++- DependencyInjection/Configuration.php | 1 + Entity/Person.php | 53 +++++++++++++ Resources/config/doctrine/Person.orm.yml | 5 ++ .../migrations/Version20160310161006.php | 49 ++++++++++++ Resources/translations/messages.fr.yml | 2 + Resources/views/Person/view.html.twig | 15 ++++ 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 Resources/migrations/Version20160310161006.php 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/Configuration.php b/DependencyInjection/Configuration.php index 5299a22fd..8a935b8b1 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -71,6 +71,7 @@ class Configuration implements ConfigurationInterface ->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 diff --git a/Entity/Person.php b/Entity/Person.php index 9f707790b..fda72bb54 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,50 @@ 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); + } + + /** + * + * @return \Chill\MainBundle\Entity\Address[]@return Address[] + */ + public function getAddresses() + { + return $this->addresses; + } + + public function getLastAddress(\DateTime $date = null) + { + if ($date === null) { + $date = new \DateTime('now'); + } + + $lastAddress = null; + + foreach ($this->getAddresses() as $address) { + if ($address->getValidFrom() < $date) { + if ($lastAddress === NULL) { + $lastAddress = $address; + } else { + if ($lastAddress->getValidFrom() < $address->getValidFrom()) { + $lastAddress = $address; + } + } + } + } + + return $lastAddress; + } + /** * Validation callback that checks if the accompanying periods are valid * diff --git a/Resources/config/doctrine/Person.orm.yml b/Resources/config/doctrine/Person.orm.yml index 838ab1e8f..60f128a11 100644 --- a/Resources/config/doctrine/Person.orm.yml +++ b/Resources/config/doctrine/Person.orm.yml @@ -72,4 +72,9 @@ Chill\PersonBundle\Entity\Person: inverseJoinColumns: language_id: referencedColumnName: id + addresses: + targetEntity: Chill\MainBundle\Entity\Address + joinTable: + name: chill_person_persons_to_addresses + cascade: [persist, remove, merge, detach] lifecycleCallbacks: { } 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 0aaccf0d0..9d7d9f364 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -70,6 +70,8 @@ Reset: 'Remise à zéro' 'Person details': 'Détails de la personne' 'Update details for %name%': 'Modifier détails de %name%' Accompanying period list: Périodes d'accompagnement +Since %date%: Depuis le %date% +No address given: Pas d'adresse renseignée #timeline 'An accompanying period is opened for %person% on %date%': Une période d'accompagnement a été ouverte le %date% pour %person% diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index e86a55857..1f17e7cb6 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' %} {# @@ -165,6 +167,19 @@ This view should receive those arguments:

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

+ {%- if chill_person.fields.address == 'visible' -%} +
+
{{ 'Address'|trans }}
+
+ {%- if person.lastAddress is not empty -%} + {{ address._render(person.lastAddress) }} + {%- else -%} + {{ 'No address given'|trans }} + {%- endif -%} +
+
+ {%- endif -%} + {%- if chill_person.fields.email == 'visible' -%}
{{ 'Email'|trans }} :
From 91ed954be825f74c358f094079067e3762f9b345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 10 Mar 2016 21:44:53 +0100 Subject: [PATCH 2/3] allow to update existing address --- Controller/PersonAddressController.php | 151 +++++++++++++++++++++++++ Resources/config/routing.yml | 8 ++ Resources/translations/messages.fr.yml | 4 + Resources/views/Address/edit.html.twig | 36 ++++++ Resources/views/Person/view.html.twig | 3 + 5 files changed, 202 insertions(+) create mode 100644 Controller/PersonAddressController.php create mode 100644 Resources/views/Address/edit.html.twig diff --git a/Controller/PersonAddressController.php b/Controller/PersonAddressController.php new file mode 100644 index 000000000..6dcea9de1 --- /dev/null +++ b/Controller/PersonAddressController.php @@ -0,0 +1,151 @@ +, + * + * 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 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_view', 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 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/Resources/config/routing.yml b/Resources/config/routing.yml index 91b481ea4..d9983e849 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -69,6 +69,14 @@ 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_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/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 9d7d9f364..959ec8db5 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -70,8 +70,12 @@ Reset: 'Remise à zéro' 'Person details': 'Détails de la personne' 'Update details for %name%': 'Modifier détails de %name%' Accompanying period list: Périodes d'accompagnement + +#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% #timeline 'An accompanying period is opened for %person% on %date%': Une période d'accompagnement a été ouverte 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..b71134745 --- /dev/null +++ b/Resources/views/Address/edit.html.twig @@ -0,0 +1,36 @@ +{# + * 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|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %} + +{% block personcontent %} + + {{ form_start(form) }} + + {{ form_row(form.streetAddress1) }} + {{ form_row(form.streetAddress2) }} + {{ form_row(form.postCode) }} + {{ form_row(form.validFrom) }} + + {{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-edit' } } ) }} + + {{ form_end(form) }} + +{% endblock personcontent %} \ No newline at end of file diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index 1f17e7cb6..a2ea48d39 100644 --- a/Resources/views/Person/view.html.twig +++ b/Resources/views/Person/view.html.twig @@ -173,6 +173,9 @@ This view should receive those arguments:
{%- if person.lastAddress is not empty -%} {{ address._render(person.lastAddress) }} + + {{ 'Edit'|trans }} + {%- else -%} {{ 'No address given'|trans }} {%- endif -%} From 8d50a9435121b93e99bbb73eebc302628f1aa67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 10 Mar 2016 21:47:58 +0100 Subject: [PATCH 3/3] fix dot in class name --- Resources/views/Person/view.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index a2ea48d39..97e1577c5 100644 --- a/Resources/views/Person/view.html.twig +++ b/Resources/views/Person/view.html.twig @@ -177,7 +177,7 @@ This view should receive those arguments: {{ 'Edit'|trans }} {%- else -%} - {{ 'No address given'|trans }} + {{ 'No address given'|trans }} {%- endif -%}