From ff751b7f30695ecdadb0a553e6d3bd9ec7bd27e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 3 Dec 2021 13:25:39 +0000 Subject: [PATCH 1/8] remove validation about UserCircleConsistency --- src/Bundle/ChillActivityBundle/Entity/Activity.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index 183b56225..419892f9b 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -46,8 +46,9 @@ use Symfony\Component\Serializer\Annotation\SerializedName; * "activity": Activity::class * }) * @ActivityValidator\ActivityValidity - * - * @UserCircleConsistency( + * + * TODO see if necessary + * UserCircleConsistency( * "CHILL_ACTIVITY_SEE_DETAILS", * getUserFunction="getUser", * path="scope") From 9244bb2f8dd9ebb6775bc570390ba958bf9cdf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 3 Dec 2021 15:06:06 +0100 Subject: [PATCH 2/8] fix pagination problems (cherry picked from commit 43702ded77449ee08b8d2c5c494c220d457373a1) --- src/Bundle/ChillMainBundle/Pagination/Paginator.php | 8 ++++++++ .../ChillMainBundle/Tests/Pagination/PaginatorTest.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Pagination/Paginator.php b/src/Bundle/ChillMainBundle/Pagination/Paginator.php index d6da55618..54fa36084 100644 --- a/src/Bundle/ChillMainBundle/Pagination/Paginator.php +++ b/src/Bundle/ChillMainBundle/Pagination/Paginator.php @@ -109,6 +109,10 @@ class Paginator implements PaginatorInterface return 1; } + if (0 === $this->totalItems) { + return 1; + } + $nb = floor($this->totalItems / $this->itemPerPage); if ($this->totalItems % $this->itemPerPage > 0) { @@ -211,6 +215,10 @@ class Paginator implements PaginatorInterface public function hasPage($number) { + if (0 === $this->totalItems) { + return 1 === $number; + } + return 0 < $number && $this->countPages() >= $number; } diff --git a/src/Bundle/ChillMainBundle/Tests/Pagination/PaginatorTest.php b/src/Bundle/ChillMainBundle/Tests/Pagination/PaginatorTest.php index 219669825..2a92ef222 100644 --- a/src/Bundle/ChillMainBundle/Tests/Pagination/PaginatorTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Pagination/PaginatorTest.php @@ -204,6 +204,14 @@ final class PaginatorTest extends KernelTestCase ); } + public function testPagesWithoutResult() + { + $paginator = $this->generatePaginator(0, 10); + + $this->assertEquals(0, $paginator->getCurrentPageFirstItemNumber()); + $this->assertEquals(10, $paginator->getItemsPerPage()); + } + /** * @param int $itemPerPage * @param string $route From 02c93389d88a29445bc7bfa2f12d990d40723678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 3 Dec 2021 15:18:45 +0100 Subject: [PATCH 3/8] fix code style (cherry picked from commit a86ba6faf53bc7fe70db95c833a13ca88e96cb3f) --- src/Bundle/ChillActivityBundle/Entity/Activity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index 419892f9b..77f650ce4 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -46,7 +46,7 @@ use Symfony\Component\Serializer\Annotation\SerializedName; * "activity": Activity::class * }) * @ActivityValidator\ActivityValidity - * + * * TODO see if necessary * UserCircleConsistency( * "CHILL_ACTIVITY_SEE_DETAILS", From 938720be52e3bce90fb133c38a15456de490abc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 6 Dec 2021 12:56:57 +0000 Subject: [PATCH 4/8] Issue316 addresses search by postal code --- CHANGELOG.md | 3 + .../AddressReferenceAPIController.php | 60 +++++++++++- .../Controller/PostalCodeAPIController.php | 75 ++++++++++++++- .../Entity/AddressReference.php | 9 ++ .../ChillMainBundle/Entity/PostalCode.php | 9 ++ .../Repository/AddressReferenceRepository.php | 91 +++++++++++++++++++ .../Repository/PostalCodeRepository.php | 85 +++++++++++++++++ .../Resources/public/vuejs/Address/api.js | 41 ++++++++- .../vuejs/Address/components/AddAddress.vue | 9 +- .../AddAddress/AddressSelection.vue | 38 +++++++- .../components/AddAddress/CitySelection.vue | 45 ++++++++- .../vuejs/Address/components/EditPane.vue | 2 + .../AddressReferenceApiControllerTest.php | 67 ++++++++++++++ .../PostalCodeApiControllerTest.php | 57 ++++++++++++ .../ChillMainBundle/chill.api.specs.yaml | 65 +++++++++++++ .../migrations/Version20211125142016.php | 85 +++++++++++++++++ .../migrations/Version20211125142017.php | 85 +++++++++++++++++ .../Controller/HouseholdMemberController.php | 2 +- 18 files changed, 805 insertions(+), 23 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/AddressReferenceApiControllerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/PostalCodeApiControllerTest.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20211125142016.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20211125142017.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ec27da83d..b81ca9783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to ## Unreleased +* [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) +* [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) + * [person search] fix bug when using birthdate after and birthdate before * [person search] increase pertinence when lastname begins with search pattern diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php index 1230cd0f6..498c77c68 100644 --- a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php @@ -12,14 +12,66 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Repository\AddressReferenceRepository; +use Chill\MainBundle\Serializer\Model\Collection; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use function trim; -/** - * Class AddressReferenceAPIController. - */ -class AddressReferenceAPIController extends ApiController +final class AddressReferenceAPIController extends ApiController { + private AddressReferenceRepository $addressReferenceRepository; + + private PaginatorFactory $paginatorFactory; + + public function __construct(AddressReferenceRepository $addressReferenceRepository, PaginatorFactory $paginatorFactory) + { + $this->addressReferenceRepository = $addressReferenceRepository; + $this->paginatorFactory = $paginatorFactory; + } + + /** + * @Route("/api/1.0/main/address-reference/by-postal-code/{id}/search.json") + */ + public function search(PostalCode $postalCode, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + if (!$request->query->has('q')) { + throw new BadRequestHttpException('You must supply a "q" parameter'); + } + + $pattern = $request->query->get('q'); + + if ('' === trim($pattern)) { + throw new BadRequestHttpException('the search pattern is empty'); + } + + $nb = $this->addressReferenceRepository->countByPostalCodePattern($postalCode, $pattern); + $paginator = $this->paginatorFactory->create($nb); + $addresses = $this->addressReferenceRepository->findByPostalCodePattern( + $postalCode, + $pattern, + false, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + + return $this->json( + new Collection($addresses, $paginator), + Response::HTTP_OK, + [], + [AbstractNormalizer::GROUPS => ['read']] + ); + } + protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('postal_code')) { diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php index fb926733b..fa1a29296 100644 --- a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php @@ -12,13 +12,80 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Repository\CountryRepository; +use Chill\MainBundle\Repository\PostalCodeRepository; +use Chill\MainBundle\Serializer\Model\Collection; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -/** - * Class PostalCodeAPIController. - */ -class PostalCodeAPIController extends ApiController +final class PostalCodeAPIController extends ApiController { + private CountryRepository $countryRepository; + + private PaginatorFactory $paginatorFactory; + + private PostalCodeRepository $postalCodeRepository; + + public function __construct( + CountryRepository $countryRepository, + PostalCodeRepository $postalCodeRepository, + PaginatorFactory $paginatorFactory + ) { + $this->countryRepository = $countryRepository; + $this->postalCodeRepository = $postalCodeRepository; + $this->paginatorFactory = $paginatorFactory; + } + + /** + * @Route("/api/1.0/main/postal-code/search.json") + */ + public function search(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + if (!$request->query->has('q')) { + throw new BadRequestHttpException('You must supply a "q" parameter'); + } + + $pattern = $request->query->get('q'); + + if ('' === trim($pattern)) { + throw new BadRequestHttpException('the search pattern is empty'); + } + + if ($request->query->has('country')) { + $country = $this->countryRepository->find($request->query->getInt('country')); + + if (null === $country) { + throw new NotFoundHttpException('country not found'); + } + } else { + $country = null; + } + + $nb = $this->postalCodeRepository->countByPattern($pattern, $country); + $paginator = $this->paginatorFactory->create($nb); + $codes = $this->postalCodeRepository->findByPattern( + $pattern, + $country, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + + return $this->json( + new Collection($codes, $paginator), + Response::HTTP_OK, + [], + [AbstractNormalizer::GROUPS => ['read']] + ); + } + protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('country')) { diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index e793167f6..99efd391d 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -22,6 +22,15 @@ use Symfony\Component\Serializer\Annotation\Groups; */ class AddressReference { + /** + * This is an internal column which is populated by database. + * + * This column will ease the search operations + * + * @ORM\Column(type="text", options={"default": ""}) + */ + private string $addressCanonical = ''; + /** * @ORM\Id * @ORM\GeneratedValue diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index 866ad04db..484a9e322 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -29,6 +29,15 @@ use Symfony\Component\Serializer\Annotation\Groups; */ class PostalCode { + /** + * This is an internal column which is populated by database. + * + * This column will ease the search operations + * + * @ORM\Column(type="text", options={"default": ""}) + */ + private string $canonical = ''; + /** * @var Point * diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php index 4344964af..6be52ec85 100644 --- a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php @@ -12,17 +12,29 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\AddressReference; +use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Persistence\ObjectRepository; +use RuntimeException; +use function explode; +use function implode; +use function strtr; +use function trim; final class AddressReferenceRepository implements ObjectRepository { + private EntityManagerInterface $entityManager; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { $this->repository = $entityManager->getRepository(AddressReference::class); + $this->entityManager = $entityManager; } public function countAll(): int @@ -33,6 +45,18 @@ final class AddressReferenceRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function countByPostalCodePattern(PostalCode $postalCode, string $pattern): int + { + $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); + $sql = $query->buildQuery(true); + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('c', 'c'); + + $nq = $this->entityManager->createNativeQuery($sql, $rsm)->setParameters($query->buildParameters(true)); + + return (int) $nq->getSingleResult()['c']; + } + public function find($id, $lockMode = null, $lockVersion = null): ?AddressReference { return $this->repository->find($id, $lockMode, $lockVersion); @@ -57,6 +81,33 @@ final class AddressReferenceRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + /** + * @return AddressReference[]|array + */ + public function findByPostalCodePattern(PostalCode $postalCode, string $pattern, bool $simplify = false, int $start = 0, int $limit = 50): array + { + $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); + + if (!$simplify) { + $rsm = new ResultSetMappingBuilder($this->entityManager); + $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'cma'); + $query->addSelectClause($rsm->generateSelectClause()); + } else { + throw new RuntimeException('not implemented'); + } + + $sql = strtr( + $query->buildQuery() . 'ORDER BY pertinence DESC, lpad(streetnumber, 10, \'0\') ASC OFFSET ? LIMIT ? ', + // little hack for adding sql method to point + ['cma.point AS point' => 'ST_AsGeojson(cma.point) AS point'] + ); + $parameters = [...$query->buildParameters(), $start, $limit]; + + return $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($parameters) + ->getResult(); + } + public function findOneBy(array $criteria, ?array $orderBy = null): ?AddressReference { return $this->repository->findOneBy($criteria, $orderBy); @@ -66,4 +117,44 @@ final class AddressReferenceRepository implements ObjectRepository { return AddressReference::class; } + + private function buildQueryByPostalCodePattern(PostalCode $postalCode, string $pattern): SearchApiQuery + { + $pattern = trim($pattern); + + if ('' === $pattern) { + throw new RuntimeException('the search pattern must not be empty'); + } + $query = new SearchApiQuery(); + + $query + ->setFromClause('chill_main_address_reference cma') + ->andWhereClause('postcode_id = ?', [$postalCode->getId()]); + + $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))']; + $pertinenceArgs = [$pattern]; + $orWhere = ['addresscanonical %>> UNACCENT(?)']; + $orWhereArgs = [$pattern]; + + foreach (explode(' ', $pattern) as $part) { + $part = trim($part); + + if ('' === $part) { + continue; + } + + $orWhere[] = "addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%'"; + $orWhereArgs[] = $part; + $pertinenceClause[] = + "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; + $pertinenceClause[] = + '(addresscanonical LIKE UNACCENT(LOWER(?)))::int'; + array_push($pertinenceArgs, $part, $part); + } + $query + ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs) + ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs); + + return $query; + } } diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php index 697c8f90d..02e63771b 100644 --- a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php @@ -11,18 +11,39 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Country; use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Persistence\ObjectRepository; +use RuntimeException; final class PostalCodeRepository implements ObjectRepository { + private EntityManagerInterface $entityManager; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { $this->repository = $entityManager->getRepository(PostalCode::class); + $this->entityManager = $entityManager; + } + + public function countByPattern(string $pattern, ?Country $country): int + { + $query = $this->buildQueryByPattern($pattern, $country); + $sql = $query->buildQuery(true); + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('c', 'c'); + + $nq = $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($query->buildParameters(true)); + + return (int) $nq->getSingleResult()['c']; } public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode @@ -49,6 +70,26 @@ final class PostalCodeRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array + { + $query = $this->buildQueryByPattern($pattern, $country); + + $rsm = new ResultSetMappingBuilder($this->entityManager); + $rsm->addRootEntityFromClassMetadata(PostalCode::class, 'cmpc'); + $query->addSelectClause($rsm->generateSelectClause()); + + $sql = strtr( + $query->buildQuery() . 'ORDER BY pertinence DESC, canonical ASC OFFSET ? LIMIT ? ', + // little hack for adding sql method to point + ['cmpc.center AS center' => 'ST_AsGeojson(cmpc.center) AS center'] + ); + $parameters = [...$query->buildParameters(), $start, $limit]; + + return $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($parameters) + ->getResult(); + } + public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode { return $this->repository->findOneBy($criteria, $orderBy); @@ -58,4 +99,48 @@ final class PostalCodeRepository implements ObjectRepository { return PostalCode::class; } + + private function buildQueryByPattern(string $pattern, ?Country $country): SearchApiQuery + { + $pattern = trim($pattern); + + if ('' === $pattern) { + throw new RuntimeException('the search pattern must not be empty'); + } + $query = new SearchApiQuery(); + + $query + ->setFromClause('chill_main_postal_code cmpc') + ->andWhereClause('cmpc.origin = 0'); + + if (null !== $country) { + $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]); + } + + $pertinenceClause = ['STRICT_WORD_SIMILARITY(canonical, UNACCENT(?))']; + $pertinenceArgs = [$pattern]; + $orWhere = ['canonical %>> UNACCENT(?)']; + $orWhereArgs = [$pattern]; + + foreach (explode(' ', $pattern) as $part) { + $part = trim($part); + + if ('' === $part) { + continue; + } + + $orWhere[] = "canonical LIKE '%' || UNACCENT(LOWER(?)) || '%'"; + $orWhereArgs[] = $part; + $pertinenceClause[] = + "(EXISTS (SELECT 1 FROM unnest(string_to_array(canonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; + $pertinenceClause[] = + '(canonical LIKE UNACCENT(LOWER(?)))::int'; + array_push($pertinenceArgs, $part, $part); + } + $query + ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs) + ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs); + + return $query; + } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js index 62065b3ad..1dbc85dee 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js @@ -16,7 +16,8 @@ const fetchCountries = () => { /** * Endpoint chill_api_single_postal_code__index -* method GET, get Country Object +* method GET, get Cities Object +* @params {object} a country object * @returns {Promise} a promise containing all Postal Code objects filtered with country */ const fetchCities = (country) => { @@ -29,6 +30,40 @@ const fetchCities = (country) => { }); }; + +/** +* Endpoint chill_main_postalcodeapi_search +* method GET, get Cities Object +* @params {string} search a search string +* @params {object} country a country object +* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string +*/ +const searchCities = (search, country) => { + const url = `/api/1.0/main/postal-code/search.json?q=${search}&country=${country.id}`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + +/** +* Endpoint chill_main_addressreferenceapi_search +* method GET, get AddressReference Object +* @params {string} search a search string +* @params {object} postalCode a postalCode object +* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string +*/ +const searchReferenceAddresses = (search, postalCode) => { + const url = `/api/1.0/main/address-reference/by-postal-code/${postalCode.id}/search.json?q=${search}`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + + /** * Endpoint chill_api_single_address_reference__index * method GET, get AddressReference Object @@ -170,5 +205,7 @@ export { postAddress, patchAddress, postPostalCode, - getAddress + getAddress, + searchCities, + searchReferenceAddresses }; 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 87ecff7fb..ed1d14633 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -556,8 +556,8 @@ export default { this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null; this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null; - this.entity.selected.writeNew.address = this.context.edit; - this.entity.selected.writeNew.postcode = this.context.edit; + this.entity.selected.writeNew.address = this.context.edit && this.entity.address.addressReference === null && this.entity.address.street.length > 0 + this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous; console.log('!! just set writeNew.postcode to', this.entity.selected.writeNew.postcode); }, @@ -569,7 +569,6 @@ export default { applyChanges() { console.log('apply changes'); - let newAddress = { 'isNoAddress': this.entity.selected.isNoAddress, 'street': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.street, @@ -633,7 +632,6 @@ export default { if (!this.context.edit) { this.addNewAddress(newAddress) .then(payload => this.addressChangedCallback(payload)); - } else { this.updateAddress({ addressId: this.context.addressId, @@ -697,8 +695,7 @@ export default { * Async PATCH transactions, * then update existing address with backend datas when promise is resolved */ - updateAddress(payload) - { + updateAddress(payload) { this.flag.loading = true; // TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode 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 2409dca53..c333961db 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 @@ -18,6 +18,7 @@ :taggable="true" :multiple="false" @tag="addAddress" + :loading="isLoading" :options="addresses"> @@ -48,14 +49,17 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js index c2ee09960..8047aea11 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js @@ -24,6 +24,7 @@ const visMessages = { refresh: "Rafraîchir", screenshot: "Prendre une photo", choose_relation: "Choisissez le lien de parenté", + relationship_household: "Filiation du ménage", }, edit: 'Éditer', del: 'Supprimer', diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js index ca76f283b..5e8989e49 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js @@ -16,7 +16,12 @@ persons.forEach(person => { }) const app = createApp({ - template: `` + template: ``, + data() { + return { + household_id: JSON.parse(container.dataset.householdId) + } + } }) .use(store) .use(i18n) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js index 119a7b29c..10269c6c8 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js @@ -112,7 +112,7 @@ const store = createStore({ } }) //console.log('array', array.map(item => item.person.id)) - console.log('get persons group', group.map(f => f.id)) + //console.log('get persons group', group.map(f => f.id)) return group }, @@ -120,13 +120,17 @@ const store = createStore({ }, mutations: { addPerson(state, [person, options]) { + let age = getAge(person) + age = (age === '')? '' : ' - ' + age + let debug = '' /// Debug mode: uncomment to display person_id on visgraph //debug = `\nid ${person.id}` + person.group = person.type person._id = person.id person.id = `person_${person.id}` - person.label = `*${person.text}*\n_${getGender(person.gender)} - ${getAge(person.birthdate)}_${debug}` // + person.label = `*${person.text}*\n_${getGender(person.gender)}${age}_${debug}` person.folded = false // folded is used for missing persons if (options.folded) { @@ -161,7 +165,7 @@ const store = createStore({ state.links.push(link) }, updateLink(state, link) { - console.log('updateLink', link) + //console.log('updateLink', link) let link_ = { from: `person_${link.fromPerson.id}`, to: `person_${link.toPerson.id}`, @@ -264,7 +268,7 @@ const store = createStore({ fetchInfoForPerson({ dispatch }, person) { // TODO enfants hors ménages // example: household 61 - // console.log(person.text, 'household', person.current_household_id) + //console.log(person.text, 'household', person.current_household_id) if (null !== person.current_household_id) { dispatch('fetchHouseholdForPerson', person) } @@ -305,15 +309,16 @@ const store = createStore({ */ addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) { let members = getters.getMembersByHousehold(household.id) - console.log('add link for', members.length, 'members') + //console.log('add link for', members.length, 'members') members.forEach(m => { commit('addLink', { from: `${m.person.type}_${m.person.id}`, - to: `household_${m.person.current_household_id}`, - id: `household_${m.person.current_household_id}-person_${m.person.id}`, + to: `${household.id}`, + id: `${household.id}-person_${m.person.id}`, arrows: 'from', color: 'pink', font: { color: '#D04A60' }, + dashes: (getHouseholdWidth(m) === 1)? [0,4] : false, //edge style: [dash, gap, dash, gap] label: getHouseholdLabel(m), width: getHouseholdWidth(m), }) @@ -362,7 +367,7 @@ const store = createStore({ */ addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) { const participations = getters.getParticipationsByCourse(course.id) - console.log('add link for', participations.length, 'participations') + //console.log('add link for', participations.length, 'participations') participations.forEach(p => { //console.log(p.person.id) commit('addLink', { @@ -445,7 +450,7 @@ const store = createStore({ * @param array */ addMissingPerson({ commit, getters, dispatch }, [person, parent]) { - console.log('! add missing Person', person.id) + //console.log('! add missing Person', person.id) commit('markPersonLoaded', person.id) commit('addPerson', [person, { folded: true }]) if (getters.isExcludedNode(parent.id)) { @@ -467,7 +472,7 @@ const store = createStore({ getters.getPersonsGroup(participations) .forEach(person => { if (person.folded === true) { - console.log('-=. unfold and expand person', person.id) + //console.log('-=. unfold and expand person', person.id) commit('unfoldPerson', person) dispatch('fetchInfoForPerson', person) } @@ -485,7 +490,7 @@ const store = createStore({ getters.getPersonsGroup(members) .forEach(person => { if (person.folded === true) { - console.log('-=. unfold and expand person', person.id) + //console.log('-=. unfold and expand person', person.id) commit('unfoldPerson', person) dispatch('fetchInfoForPerson', person) } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js index e95bc0d0b..3e00db883 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js @@ -13,13 +13,12 @@ window.options = { locale: 'fr', locales: visMessages, /* + */ configure: { enabled: true, - filter: 'nodes,edges', - //container: undefined, + filter: 'physics', showButton: true }, - */ physics: { enabled: true, barnesHut: { @@ -37,8 +36,8 @@ window.options = { centralGravity: 0.01, springLength: 100, springConstant: 0.08, - damping: 0.4, - avoidOverlap: 0 + damping: 0.75, + avoidOverlap: 0.00 }, repulsion: { centralGravity: 0.2, @@ -159,17 +158,21 @@ const getGender = (gender) => { } /** - * TODO Repeat getAge() in PersonRenderBox.vue - * @param birthdate + * TODO only one abstract function (-> getAge() is repeated in PersonRenderBox.vue) + * @param person * @returns {string|null} */ -const getAge = (birthdate) => { - if (null === birthdate) { - return null +const getAge = (person) => { + if (person.birthdate) { + let birthdate = new Date(person.birthdate.datetime) + if (person.deathdate) { + let deathdate = new Date(person.deathdate.datetime) + return (deathdate.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years + } + let now = new Date() + return (now.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years } - const birthday = new Date(birthdate.datetime) - const now = new Date() - return (now.getFullYear() - birthday.getFullYear()) + ' '+ visMessages.fr.visgraph.years + return '' } /** diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue index ffb9ef3d4..ab7074ffc 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue @@ -192,6 +192,7 @@ export default { return `/fr/person/${this.person.id}/general`; }, getAge: function() { + // TODO only one abstract function if(this.person.birthdate && !this.person.deathdate){ const birthday = new Date(this.person.birthdate.datetime) const now = new Date() diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig index 56fcce85c..acaeebb96 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig @@ -17,7 +17,8 @@
+ data-persons="{{ persons|e('html_attr') }}" + data-household-id="{{ household.id|e('html_attr') }}">
{% endblock %} From 07f53e67586a291bc2240ae77ca5e020ed02527f Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Mon, 6 Dec 2021 14:07:21 +0000 Subject: [PATCH 8/8] titulaire field removed from household edit form --- CHANGELOG.md | 1 + .../ChillPersonBundle/Form/HouseholdMemberType.php | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abc7a219b..ba2c90409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to * [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) * [person search] fix bug when using birthdate after and birthdate before * [person search] increase pertinence when lastname begins with search pattern +* [household] field to edit wheter person is titulaire of household or not removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/322) * [activity] create work if a work with same social action is not associated to the activity * [visgraph] improve and fix bugs on vis-network relationship graph * [bugfix] posting of birth- and deathdate through api fixed. diff --git a/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php b/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php index a5284afdb..e0d2d9d47 100644 --- a/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php +++ b/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php @@ -14,7 +14,6 @@ namespace Chill\PersonBundle\Form; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; class HouseholdMemberType extends AbstractType @@ -26,17 +25,6 @@ class HouseholdMemberType extends AbstractType 'label' => 'household.Start date', 'input' => 'datetime_immutable', ]); - - if ($options['data']->getPosition()->isAllowHolder()) { - $builder - ->add('holder', ChoiceType::class, [ - 'label' => 'household.holder', - 'choices' => [ - 'household.is holder' => true, - 'household.is not holder' => false, - ], - ]); - } $builder ->add('comment', ChillTextareaType::class, [ 'label' => 'household.Comment',