mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Issue316 addresses search by postal code
This commit is contained in:
parent
51bbcab878
commit
938720be52
@ -11,6 +11,9 @@ and this project adheres to
|
||||
## Unreleased
|
||||
|
||||
<!-- write down unreleased development here -->
|
||||
* [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
|
||||
|
||||
|
@ -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')) {
|
||||
|
@ -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')) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -18,6 +18,7 @@
|
||||
:taggable="true"
|
||||
:multiple="false"
|
||||
@tag="addAddress"
|
||||
:loading="isLoading"
|
||||
:options="addresses">
|
||||
</VueMultiselect>
|
||||
</div>
|
||||
@ -48,14 +49,17 @@
|
||||
|
||||
<script>
|
||||
import VueMultiselect from 'vue-multiselect';
|
||||
import { searchReferenceAddresses, fetchReferenceAddresses } from '../../api.js';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'AddressSelection',
|
||||
components: { VueMultiselect },
|
||||
props: ['entity', 'updateMapCenter'],
|
||||
props: ['entity', 'context', 'updateMapCenter'],
|
||||
data() {
|
||||
return {
|
||||
value: null
|
||||
value: this.context.edit ? this.entity.address.addressReference : null,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -107,6 +111,36 @@ export default {
|
||||
},
|
||||
listenInputSearch(query) {
|
||||
//console.log('listenInputSearch', query, this.isAddressSelectorOpen);
|
||||
if (!this.entity.selected.writeNew.postcode) {
|
||||
if (query.length > 2) {
|
||||
this.isLoading = true;
|
||||
searchReferenceAddresses(query, this.entity.selected.city).then(
|
||||
addresses => new Promise((resolve, reject) => {
|
||||
this.entity.loaded.addresses = addresses.results;
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.log(error); //TODO better error handling
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
if (query.length === 0) { // Fetch all cities when suppressing the query
|
||||
this.isLoading = true;
|
||||
fetchReferenceAddresses(this.entity.selected.city).then(
|
||||
addresses => new Promise((resolve, reject) => {
|
||||
this.entity.loaded.addresses = addresses.results;
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isAddressSelectorOpen) {
|
||||
this.$data.value = { text: query };
|
||||
} else if (this.isEnteredCustomAddress) {
|
||||
|
@ -18,6 +18,7 @@
|
||||
:multiple="false"
|
||||
@tag="addPostcode"
|
||||
:tagPlaceholder="$t('create_postal_code')"
|
||||
:loading="isLoading"
|
||||
:options="cities">
|
||||
</VueMultiselect>
|
||||
</div>
|
||||
@ -48,15 +49,17 @@
|
||||
|
||||
<script>
|
||||
import VueMultiselect from 'vue-multiselect';
|
||||
import { searchCities, fetchCities } from '../../api.js';
|
||||
|
||||
export default {
|
||||
name: 'CitySelection',
|
||||
components: { VueMultiselect },
|
||||
props: ['entity', 'focusOnAddress', 'updateMapCenter'],
|
||||
props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter'],
|
||||
emits: ['getReferenceAddresses'],
|
||||
data() {
|
||||
return {
|
||||
value: null
|
||||
value: this.context.edit ? this.entity.address.postcode : null,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -93,6 +96,15 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
console.log('writeNew.postcode', this.entity.selected.writeNew.postcode, 'in mounted');
|
||||
if (this.context.edit) {
|
||||
this.entity.selected.city = this.value;
|
||||
this.entity.selected.postcode.name = this.value.name;
|
||||
this.entity.selected.postcode.code = this.value.code;
|
||||
this.$emit('getReferenceAddresses', this.value);
|
||||
if (this.value.center) {
|
||||
this.updateMapCenter(this.value.center);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
transName(value) {
|
||||
@ -105,7 +117,6 @@ export default {
|
||||
this.entity.selected.postcode.code = value.code;
|
||||
this.entity.selected.postcode.coordinates = value.center.coordinates;
|
||||
this.entity.selected.writeNew.postcode = false;
|
||||
console.log('writeNew.postcode false, in selectCity');
|
||||
this.$emit('getReferenceAddresses', value);
|
||||
this.focusOnAddress();
|
||||
if (value.center) {
|
||||
@ -113,7 +124,33 @@ export default {
|
||||
}
|
||||
},
|
||||
listenInputSearch(query) {
|
||||
//console.log('listenInputSearch', query, this.isCitySelectorOpen);
|
||||
if (query.length > 2) {
|
||||
this.isLoading = true;
|
||||
searchCities(query, this.entity.selected.country).then(
|
||||
cities => new Promise((resolve, reject) => {
|
||||
this.entity.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.log(error); //TODO better error handling
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
if (query.length === 0) { // Fetch all cities when suppressing the query
|
||||
this.isLoading = true;
|
||||
fetchCities(this.entity.selected.country).then(
|
||||
cities => new Promise((resolve, reject) => {
|
||||
this.entity.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.isCitySelectorOpen) {
|
||||
this.$data.value = { text: query };
|
||||
} else if (this.isEnteredCustomCity) {
|
||||
|
@ -30,6 +30,7 @@
|
||||
|
||||
<city-selection
|
||||
v-bind:entity="entity"
|
||||
v-bind:context="context"
|
||||
v-bind:focusOnAddress="focusOnAddress"
|
||||
v-bind:updateMapCenter="updateMapCenter"
|
||||
@getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
|
||||
@ -37,6 +38,7 @@
|
||||
|
||||
<address-selection v-if="!isNoAddress"
|
||||
v-bind:entity="entity"
|
||||
v-bind:context="context"
|
||||
v-bind:updateMapCenter="updateMapCenter">
|
||||
</address-selection>
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers.
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class AddressReferenceApiControllerTest extends WebTestCase
|
||||
{
|
||||
use PrepareClientTrait;
|
||||
|
||||
public function provideData()
|
||||
{
|
||||
self::bootKernel();
|
||||
/** @var EntityManagerInterface $em */
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
$postalCode = $em->createQueryBuilder()
|
||||
->select('pc')
|
||||
->from(PostalCode::class, 'pc')
|
||||
->where('pc.origin = :origin')
|
||||
->setParameter('origin', 0)
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
|
||||
yield [$postalCode->getId(), 'rue'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideData
|
||||
*/
|
||||
public function testSearch(int $postCodeId, string $pattern)
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$client->request(
|
||||
'GET',
|
||||
"/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
|
||||
['q' => $pattern]
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers.
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use function json_decode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class PostalCodeApiControllerTest extends WebTestCase
|
||||
{
|
||||
use PrepareClientTrait;
|
||||
|
||||
public function testSearch()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$client->request(
|
||||
'GET',
|
||||
'/api/1.0/main/postal-code/search.json',
|
||||
['q' => 'fontenay le comte']
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertEquals('Fontenay Le Comte', $data['results'][0]['name']);
|
||||
|
||||
// test response with invalid search pattern
|
||||
$client->request(
|
||||
'GET',
|
||||
'/api/1.0/main/postal-code/search.json',
|
||||
['q' => '']
|
||||
);
|
||||
$this->assertResponseStatusCodeSame(400);
|
||||
}
|
||||
}
|
@ -360,6 +360,40 @@ paths:
|
||||
401:
|
||||
description: "Unauthorized"
|
||||
|
||||
/1.0/main/address-reference/by-postal-code/{id}/search.json:
|
||||
get:
|
||||
tags:
|
||||
- address
|
||||
- search
|
||||
summary: Return a reference address by id
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The reference address id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
description: The search pattern
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "ok"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddressReference'
|
||||
404:
|
||||
description: "not found"
|
||||
401:
|
||||
description: "Unauthorized"
|
||||
400:
|
||||
description: "Bad request"
|
||||
/1.0/main/postal-code.json:
|
||||
get:
|
||||
tags:
|
||||
@ -430,6 +464,37 @@ paths:
|
||||
401:
|
||||
description: "Unauthorized"
|
||||
|
||||
/1.0/main/postal-code/search.json:
|
||||
get:
|
||||
tags:
|
||||
- address
|
||||
- search
|
||||
summary: Search a postal code
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
description: The search pattern
|
||||
schema:
|
||||
type: string
|
||||
- name: country
|
||||
in: query
|
||||
required: false
|
||||
description: The country id
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
200:
|
||||
description: "ok"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PostalCode'
|
||||
404:
|
||||
description: "not found"
|
||||
400:
|
||||
description: "Bad Request"
|
||||
|
||||
/1.0/main/country.json:
|
||||
get:
|
||||
tags:
|
||||
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211125142016 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TRIGGER canonicalize_address_reference_on_insert ON chill_main_address_reference');
|
||||
$this->addSql('DROP TRIGGER canonicalize_address_reference_on_update ON chill_main_address_reference');
|
||||
$this->addSql('DROP FUNCTION canonicalize_address_reference()');
|
||||
$this->addSql('ALTER TABLE chill_main_address_reference DROP COLUMN addressCanonical');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a column "canonicalized" on chill_main_address_reference and add trigger and indexed on it';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_address_reference ADD addressCanonical TEXT DEFAULT \'\' NOT NULL');
|
||||
|
||||
$this->addSql('UPDATE chill_main_address_reference
|
||||
SET addresscanonical =
|
||||
TRIM(
|
||||
UNACCENT(
|
||||
LOWER(
|
||||
street ||
|
||||
\' \' ||
|
||||
streetnumber
|
||||
)
|
||||
)
|
||||
|
||||
)');
|
||||
|
||||
$this->addSql('CREATE OR REPLACE FUNCTION public.canonicalize_address_reference() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS
|
||||
$$
|
||||
BEGIN
|
||||
NEW.addresscanonical =
|
||||
TRIM(
|
||||
UNACCENT(
|
||||
LOWER(
|
||||
NEW.street ||
|
||||
\' \' ||
|
||||
NEW.streetnumber
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
;
|
||||
|
||||
return NEW;
|
||||
END
|
||||
$$');
|
||||
|
||||
$this->addSql('CREATE TRIGGER canonicalize_address_reference_on_insert
|
||||
BEFORE INSERT
|
||||
ON chill_main_address_reference
|
||||
FOR EACH ROW
|
||||
EXECUTE procedure canonicalize_address_reference()');
|
||||
|
||||
$this->addSql('CREATE TRIGGER canonicalize_address_reference_on_update
|
||||
BEFORE UPDATE
|
||||
ON chill_main_address_reference
|
||||
FOR EACH ROW
|
||||
EXECUTE procedure canonicalize_address_reference()');
|
||||
|
||||
$this->addSql('CREATE INDEX chill_internal_address_reference_canonicalized ON chill_main_address_reference USING GIST (postcode_id, addressCanonical gist_trgm_ops)');
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211125142017 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TRIGGER canonicalize_postal_code_on_insert ON chill_main_postal_code');
|
||||
$this->addSql('DROP TRIGGER canonicalize_postal_code_on_update ON chill_main_postal_code');
|
||||
$this->addSql('DROP FUNCTION canonicalize_postal_code()');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP COLUMN canonical');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a column "canonicalized" on postal code';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD canonical TEXT DEFAULT \'\' NOT NULL');
|
||||
|
||||
$this->addSql('UPDATE chill_main_postal_code
|
||||
SET canonical =
|
||||
TRIM(
|
||||
UNACCENT(
|
||||
LOWER(
|
||||
code ||
|
||||
\' \' ||
|
||||
label
|
||||
)
|
||||
)
|
||||
|
||||
)');
|
||||
|
||||
$this->addSql('CREATE OR REPLACE FUNCTION public.canonicalize_postal_code() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS
|
||||
$$
|
||||
BEGIN
|
||||
NEW.canonical =
|
||||
TRIM(
|
||||
UNACCENT(
|
||||
LOWER(
|
||||
NEW.code ||
|
||||
\' \' ||
|
||||
NEW.label
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
;
|
||||
|
||||
return NEW;
|
||||
END
|
||||
$$');
|
||||
|
||||
$this->addSql('CREATE TRIGGER canonicalize_postal_code_on_insert
|
||||
BEFORE INSERT
|
||||
ON chill_main_postal_code
|
||||
FOR EACH ROW
|
||||
EXECUTE procedure canonicalize_postal_code()');
|
||||
|
||||
$this->addSql('CREATE TRIGGER canonicalize_postal_code_on_update
|
||||
BEFORE UPDATE
|
||||
ON chill_main_postal_code
|
||||
FOR EACH ROW
|
||||
EXECUTE procedure canonicalize_postal_code()');
|
||||
|
||||
$this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0');
|
||||
}
|
||||
}
|
@ -186,7 +186,7 @@ class HouseholdMemberController extends ApiController
|
||||
$_format,
|
||||
['groups' => ['read']]
|
||||
);
|
||||
} catch (Exception\InvalidArgumentException|Exception\UnexpectedValueException $e) {
|
||||
} catch (Exception\InvalidArgumentException | Exception\UnexpectedValueException $e) {
|
||||
throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user