From 05b2b2f9b88735f5e7e932982d280a710c409ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 29 Sep 2021 23:55:54 +0200 Subject: [PATCH] Link between address and reference, and api endpoint to find household by address reference * add a one-to-many link between address and address reference; * update AddAddress.vue to add information on picked address reference; * add an HouseholdACLAwareRepository, with a method to find household by current address reference * add an endpoint to retrieve household by address reference * + tests --- src/Bundle/ChillMainBundle/Entity/Address.php | 57 +++++++--- .../vuejs/Address/components/AddAddress.vue | 11 ++ .../AddAddress/AddressSelection.vue | 3 + .../Normalizer/AddressNormalizer.php | 4 + .../migrations/Version20210929192242.php | 31 ++++++ .../Controller/HouseholdApiController.php | 38 ++++++- .../Household/HouseholdACLAwareRepository.php | 103 ++++++++++++++++++ .../HouseholdACLAwareRepositoryInterface.php | 23 ++++ .../Security/Authorization/HouseholdVoter.php | 8 ++ .../Controller/HouseholdApiControllerTest.php | 80 +++++++++++++- .../ChillPersonBundle/chill.api.specs.yaml | 26 +++++ .../config/services/repository.yaml | 2 + 12 files changed, 365 insertions(+), 21 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20210929192242.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php create mode 100644 src/Bundle/ChillPersonBundle/Security/Authorization/HouseholdVoter.php diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 4cc5c37ba..06839ec99 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -23,7 +23,7 @@ class Address * @ORM\Id * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") - * @groups({"write"}) + * @Groups({"write"}) */ private $id; @@ -31,7 +31,7 @@ class Address * @var string * * @ORM\Column(type="string", length=255) - * @groups({"write"}) + * @Groups({"write"}) */ private $street = ''; @@ -39,7 +39,7 @@ class Address * @var string * * @ORM\Column(type="string", length=255) - * @groups({"write"}) + * @Groups({"write"}) */ private $streetNumber = ''; @@ -47,7 +47,7 @@ class Address * @var PostalCode * * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") - * @groups({"write"}) + * @Groups({"write"}) */ private $postcode; @@ -55,7 +55,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $floor; @@ -63,7 +63,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $corridor; @@ -71,7 +71,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $steps; @@ -79,7 +79,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $buildingName; @@ -87,7 +87,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $flat; @@ -95,7 +95,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $distribution; @@ -103,7 +103,7 @@ class Address * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $extra; @@ -114,7 +114,7 @@ class Address * @var \DateTime * * @ORM\Column(type="date") - * @groups({"write"}) + * @Groups({"write"}) */ private \DateTime $validFrom; @@ -125,13 +125,13 @@ class Address * @var \DateTime|null * * @ORM\Column(type="date", nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private ?\DateTime $validTo = null; /** * True if the address is a "no address", aka homeless person, ... - * @groups({"write"}) + * @Groups({"write"}) * @ORM\Column(type="boolean") * * @var bool @@ -144,7 +144,7 @@ class Address * @var Point|null * * @ORM\Column(type="point", nullable=true) - * @groups({"write"}) + * @Groups({"write"}) */ private $point; @@ -154,7 +154,7 @@ class Address * @var ThirdParty|null * * @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") - * @groups({"write"}) + * @Groups({"write"}) * @ORM\JoinColumn(nullable=true, onDelete="SET NULL") */ private $linkedToThirdParty; @@ -166,6 +166,12 @@ class Address */ private $customs = []; + /** + * @ORM\ManyToOne(targetEntity=AddressReference::class) + * @Groups({"write"}) + */ + private ?AddressReference $addressReference = null; + public function __construct() { $this->validFrom = new \DateTime(); @@ -376,6 +382,7 @@ class Address public static function createFromAddress(Address $original) : Address { return (new Address()) + ->setAddressReference($original->getAddressReference()) ->setBuildingName($original->getBuildingName()) ->setCorridor($original->getCorridor()) ->setCustoms($original->getCustoms()) @@ -402,6 +409,7 @@ class Address ->setPostcode($original->getPostcode()) ->setStreet($original->getStreet()) ->setStreetNumber($original->getStreetNumber()) + ->setAddressReference($original) ; } @@ -549,5 +557,22 @@ class Address return $this; } + /** + * @return AddressReference|null + */ + public function getAddressReference(): ?AddressReference + { + return $this->addressReference; + } + + /** + * @param AddressReference|null $addressReference + * @return Address + */ + public function setAddressReference(?AddressReference $addressReference = null): Address + { + $this->addressReference = $addressReference; + return $this; + } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 5616ce0be..7160d7cec 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -589,6 +589,14 @@ export default { 'point': this.entity.selected.address.point.coordinates }); } + + // add the address reference, if any + if (this.entity.selected.address.addressReference !== undefined) { + newAddress = Object.assign(newAddress, { + 'addressReference': this.entity.selected.address.addressReference + }); + } + if (this.validFrom) { console.log('add validFrom in fetch body', this.entity.selected.valid.from); newAddress = Object.assign(newAddress, { @@ -733,6 +741,9 @@ export default { }, /** + * + * Called when the event pick-address is emitted, which is, by the way, + * called when an address suggestion is picked. * * @param address the address selected */ diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue index 50a1bd8c1..ca2f5d634 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue @@ -95,6 +95,9 @@ export default { }, selectAddress(value) { this.entity.selected.address = value; + this.entity.selected.address.addressReference = { + id: value.id + }; this.entity.selected.address.street = value.street; this.entity.selected.address.streetNumber = value.streetNumber; this.entity.selected.writeNew.address = false; diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php index 2b39c07c3..ee628e014 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php @@ -3,6 +3,7 @@ namespace Chill\MainBundle\Serializer\Normalizer; use Chill\MainBundle\Entity\Address; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -33,6 +34,9 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface $data['extra'] = $address->getExtra(); $data['validFrom'] = $address->getValidFrom(); $data['validTo'] = $address->getValidTo(); + $data['addressReference'] = $this->normalizer->normalize($address->getAddressReference(), $format, [ + AbstractNormalizer::GROUPS => ['read'] + ]); return $data; } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php b/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php new file mode 100644 index 000000000..a2fbefa69 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE chill_main_address ADD addressReference_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F647069464 FOREIGN KEY (addressReference_id) REFERENCES chill_main_address_reference (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_165051F647069464 ON chill_main_address (addressReference_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_address DROP addressReference_id'); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php index 0bb804689..11e41ba29 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php @@ -4,24 +4,31 @@ namespace Chill\PersonBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\AddressReference; use Chill\MainBundle\Serializer\Model\Collection; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Household\Household; +use Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\Household\HouseholdRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; class HouseholdApiController extends ApiController { private HouseholdRepository $householdRepository; - public function __construct(HouseholdRepository $householdRepository) - { + private HouseholdACLAwareRepositoryInterface $householdACLAwareRepository; + + public function __construct( + HouseholdRepository $householdRepository, + HouseholdACLAwareRepositoryInterface $householdACLAwareRepository + ) { $this->householdRepository = $householdRepository; + $this->householdACLAwareRepository = $householdACLAwareRepository; } - public function householdAddressApi($id, Request $request, string $_format): Response { @@ -37,7 +44,7 @@ class HouseholdApiController extends ApiController { // TODO add acl - $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); + $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); $paginator = $this->getPaginatorFactory()->create($count); if ($count === 0) { @@ -93,4 +100,27 @@ class HouseholdApiController extends ApiController return $this->json(\array_values($addresses), Response::HTTP_OK, [], [ 'groups' => [ 'read' ] ]); } + + /** + * + * @Route("/api/1.0/person/household/by-address-reference/{id}.json", + * name="chill_api_person_household_by_address_reference") + * @param AddressReference $addressReference + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getHouseholdByAddressReference(AddressReference $addressReference): Response + { + // TODO ACL + $this->denyAccessUnlessGranted('ROLE_USER'); + + $total = $this->householdACLAwareRepository->countByAddressReference($addressReference); + $paginator = $this->getPaginatorFactory()->create($total); + $households = $this->householdACLAwareRepository->findByAddressReference($addressReference, + $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage()); + $collection = new Collection($households, $paginator); + + return $this->json($collection, Response::HTTP_OK, [], [ + AbstractNormalizer::GROUPS => ['read'] + ]); + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php new file mode 100644 index 000000000..38236df14 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php @@ -0,0 +1,103 @@ +em = $em; + $this->authorizationHelper = $authorizationHelper; + $this->security = $security; + } + + public function countByAddressReference(AddressReference $addressReference): int + { + $qb = $this->buildQueryByAddressReference($addressReference); + $qb = $this->addACL($qb); + + return $qb->select('COUNT(h)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function findByAddressReference( + AddressReference $addressReference, + ?int $firstResult = 0, + ?int $maxResult = 50 + ): array { + $qb = $this->buildQueryByAddressReference($addressReference); + $qb = $this->addACL($qb); + + return $qb + ->select('h') + ->setFirstResult($firstResult) + ->setMaxResults($maxResult) + ->getQuery() + ->getResult(); + } + + public function buildQueryByAddressReference(AddressReference $addressReference): QueryBuilder + { + $qb = $this->em->createQueryBuilder(); + $qb + ->select('h') + ->from(Household::class, 'h') + ->join('h.addresses', 'address') + ->where( + $qb->expr()->eq('address.addressReference', ':reference') + ) + ->setParameter(':reference', $addressReference) + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte('address.validFrom', ':today'), + $qb->expr()->orX( + $qb->expr()->isNull('address.validTo'), + $qb->expr()->gt('address.validTo', ':today') + ) + ) + ) + ->setParameter('today', new \DateTime('today')) + ; + + return $qb; + } + + public function addACL(QueryBuilder $qb, string $alias = 'h'): QueryBuilder + { + $centers = $this->authorizationHelper->getReachableCenters( + $this->security->getUser(), + HouseholdVoter::SHOW + ); + + if ([] === $centers) { + return $qb + ->andWhere("'FALSE' = 'TRUE'"); + } + + $qb + ->join($alias.'.members', 'members') + ->join('members.person', 'person') + ->andWhere( + $qb->expr()->in('person.center', ':centers') + ) + ->setParameter('centers', $centers); + + return $qb; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php new file mode 100644 index 000000000..56a927ae5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php @@ -0,0 +1,23 @@ +assertResponseIsSuccessful(); } + /** + * @dataProvider generateHouseholdAssociatedWithAddressReference + */ + public function testFindHouseholdByAddressReference(int $addressReferenceId, int $expectedHouseholdId) + { + $client = $this->getClientAuthenticated(); + + $client->request( + Request::METHOD_GET, + "/api/1.0/person/household/by-address-reference/$addressReferenceId.json" + ); + + $this->assertResponseIsSuccessful(); + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertArrayHasKey('count', $data); + $this->assertArrayHasKey('results', $data); + + $householdIds = \array_map(function($r) { + return $r['id']; + }, $data['results']); + + $this->assertContains($expectedHouseholdId, $householdIds); + } + + public function generateHouseholdAssociatedWithAddressReference() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + $centerA = $em->getRepository(Center::class)->findOneBy(['name' => 'Center A']); + $nbReference = $em->createQueryBuilder()->select('count(ar)')->from(AddressReference::class, 'ar') + ->getQuery()->getSingleScalarResult(); + $reference = $em->createQueryBuilder()->select('ar')->from(AddressReference::class, 'ar') + ->setFirstResult(\random_int(0, $nbReference)) + ->setMaxResults(1) + ->getQuery()->getSingleResult(); + $p = new Person(); + $p->setFirstname('test')->setLastName('test lastname') + ->setGender(Person::BOTH_GENDER) + ->setCenter($centerA) + ; + $em->persist($p); + $h = new Household(); + $h->addMember($m = (new HouseholdMember())->setPerson($p)); + $h->addAddress(Address::createFromAddressReference($reference)->setValidFrom(new \DateTime('today'))); + $em->persist($m); + $em->persist($h); + + $em->flush(); + + $this->toDelete = $this->toDelete + [ + [HouseholdMember::class, $m->getId()], + [User::class, $p->getId()], + [Household::class, $h->getId()] + ]; + + yield [$reference->getId(), $h->getId()]; + } + + protected function tearDown() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach ($this->toDelete as list($class, $id)) { + $obj = $em->getRepository($class)->find($id); + $em->remove($obj); + } + + $em->flush(); + } + public function generatePersonId() { self::bootKernel(); @@ -64,7 +142,7 @@ class HouseholdApiControllerTest extends WebTestCase ; $person = $period->getParticipations() - ->first()->getPerson(); + ->first()->getPerson(); yield [ $person->getId() ]; } diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index 646d1fb19..4041c52b7 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -1127,6 +1127,32 @@ paths: 401: description: "Unauthorized" + /1.0/person/household/by-address-reference/{address_id}.json: + get: + tags: + - household + summary: Return a list of household which are sharing the same address reference + parameters: + - name: address_id + in: path + required: true + description: the address reference id + schema: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: "#/components/schemas/Household" + 404: + description: "not found" + 401: + description: "Unauthorized" + /1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json: get: tags: diff --git a/src/Bundle/ChillPersonBundle/config/services/repository.yaml b/src/Bundle/ChillPersonBundle/config/services/repository.yaml index b4d179dd1..6cb9e88fd 100644 --- a/src/Bundle/ChillPersonBundle/config/services/repository.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/repository.yaml @@ -10,3 +10,5 @@ services: Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\PersonACLAwareRepository' Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository' + + Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepository'