mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'features/person-search-improve' into 'master'
refactor search for using search by pertinence See merge request Chill-Projet/chill-bundles!224 # Description of changes * remplacer le classement par ordre alphabétique par un classement par ordre de pertinence, qui tient compte: * de la présence d'une string avec le nom de la ville; * de la similarité; * du fait que la recherche commence par une partie du mot recherché * ajouter la recherche par numéro de téléphone directement dans la barre de recherche et dans le formulaire recherche avancée; * ajouter la recherche par date de naissance directement dans la barre de recherche; * ajouter la recherche par ville dans la recherche avancée * ajouter un lien vers le ménage dans les résultats de recherche * ajouter l'id du parcours dans les résultats de recherche * ajouter le demandeur dans les résultats de recherche * ajout d'un bouton "recherche avancée" sur la page d'accueil * inclure les droits de vendée dans la recherche par l'api # Issues related * https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/300 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/182 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/193 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/212 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/169 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/149 * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/156 # Tests * add tests for extracting phonenumber * add tests for extracting dates * test for search still exists
This commit is contained in:
commit
46a4afe1b3
25
CHANGELOG.md
25
CHANGELOG.md
@ -11,8 +11,28 @@ and this project adheres to
|
||||
## Unreleased
|
||||
|
||||
<!-- write down unreleased development here -->
|
||||
|
||||
* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167)
|
||||
* remove "search by phone configuration option": search by phone is now executed by default
|
||||
* remplacer le classement par ordre alphabétique par un classement par ordre de pertinence, qui tient compte:
|
||||
* de la présence d'une string avec le nom de la ville;
|
||||
* de la similarité;
|
||||
* du fait que la recherche commence par une partie du mot recherché
|
||||
* ajouter la recherche par numéro de téléphone directement dans la barre de recherche et dans le formulaire recherche avancée;
|
||||
* ajouter la recherche par date de naissance directement dans la barre de recherche;
|
||||
* ajouter la recherche par ville dans la recherche avancée
|
||||
* ajouter un lien vers le ménage dans les résultats de recherche
|
||||
* ajouter l'id du parcours dans les résultats de recherche
|
||||
* ajouter le demandeur dans les résultats de recherche
|
||||
* ajout d'un bouton "recherche avancée" sur la page d'accueil
|
||||
* [person] create an accompanying course: add client-side validation if no origin (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/210)
|
||||
* [person] fix bounds for computing current person address: the new address appears immediatly
|
||||
|
||||
|
||||
## Test releases
|
||||
|
||||
### Test release 2021-11-15
|
||||
|
||||
* [main] fix adding multiple AddresseDeRelais (combine PickAddressType with ChillCollection)
|
||||
* [person]: do not suggest the current household of the person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/51)
|
||||
* [person]: display other phone numbers in view + add message in case no others phone numbers (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/184)
|
||||
@ -26,11 +46,6 @@ and this project adheres to
|
||||
* [person suggest] In widget "add person", improve the pertinence of persons when one of the names starts with the pattern;
|
||||
* [person] do not ask for center any more on person creation
|
||||
* [3party] do not ask for center any more on 3party creation
|
||||
* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167)
|
||||
* [accompanying course] Add associated persons in banner details. social-issues and associated-persons are slides in same space.
|
||||
|
||||
|
||||
## Test releases
|
||||
|
||||
### Test release 2021-11-08
|
||||
|
||||
|
@ -705,11 +705,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\PersonBundle\\\\Search\\\\SimilarityPersonSearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php
|
||||
|
||||
-
|
||||
message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#"
|
||||
count: 1
|
||||
|
@ -1,4 +1,2 @@
|
||||
|
||||
{{ dump(notification) }}
|
||||
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Activity;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173555 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'activitytype.name',
|
||||
'activitytypecategory.name'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173557 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_calendar.cancel_reason.name',
|
||||
'chill_calendar.invite.status',
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\DocGenerator;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173556 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_docgen_template.name'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173558 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_doc.document_category.name',
|
||||
'chill_doc.stored_object.key',
|
||||
'chill_doc.stored_object.iv',
|
||||
'chill_doc.stored_object.datas',
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace Chill\MainBundle;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleInterface;
|
||||
@ -41,6 +42,8 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill_main.scope_resolver');
|
||||
$container->registerForAutoconfiguration(ChillEntityRenderInterface::class)
|
||||
->addTag('chill.render_entity');
|
||||
$container->registerForAutoconfiguration(SearchApiInterface::class)
|
||||
->addTag('chill.search_api_provider');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass());
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||
|
@ -5,7 +5,7 @@
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>{{ installation.name }} - {% block title %}{% endblock %}</title>
|
||||
<title>{{ installation.name }} - {% block title %}{{ 'Homepage'|trans }}{% endblock %}</title>
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
@ -68,6 +68,9 @@
|
||||
<button type="submit" class="btn btn-lg btn-warning mt-3">
|
||||
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
|
||||
</button>
|
||||
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
|
||||
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
|
||||
</a>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -26,10 +26,10 @@ use Chill\MainBundle\Search\ParsingException;
|
||||
|
||||
/**
|
||||
* This class implements abstract search with most common responses.
|
||||
*
|
||||
*
|
||||
* you should use this abstract class instead of SearchInterface : if the signature of
|
||||
* search interface change, the generic method will be implemented here.
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
*/
|
||||
@ -37,7 +37,7 @@ abstract class AbstractSearch implements SearchInterface
|
||||
{
|
||||
/**
|
||||
* parse string expected to be a date and transform to a DateTime object
|
||||
*
|
||||
*
|
||||
* @param type $string
|
||||
* @return \DateTime
|
||||
* @throws ParsingException if the date is not parseable
|
||||
@ -51,14 +51,14 @@ abstract class AbstractSearch implements SearchInterface
|
||||
. 'not parsable', 0, $ex);
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* recompose a pattern, retaining only supported terms
|
||||
*
|
||||
*
|
||||
* the outputted string should be used to show users their search
|
||||
*
|
||||
*
|
||||
* @param array $terms
|
||||
* @param array $supportedTerms
|
||||
* @param string $domain if your domain is NULL, you should set NULL. You should set used domain instead
|
||||
@ -67,35 +67,35 @@ abstract class AbstractSearch implements SearchInterface
|
||||
protected function recomposePattern(array $terms, array $supportedTerms, $domain = NULL)
|
||||
{
|
||||
$recomposed = '';
|
||||
|
||||
|
||||
if ($domain !== NULL)
|
||||
{
|
||||
$recomposed .= '@'.$domain.' ';
|
||||
}
|
||||
|
||||
|
||||
foreach ($supportedTerms as $term) {
|
||||
if (array_key_exists($term, $terms) && $term !== '_default') {
|
||||
$recomposed .= ' '.$term.':';
|
||||
$containsSpace = \strpos($terms[$term], " ") !== false;
|
||||
if ($containsSpace) {
|
||||
$recomposed .= "(";
|
||||
$recomposed .= '"';
|
||||
}
|
||||
$recomposed .= (mb_stristr(' ', $terms[$term]) === FALSE) ? $terms[$term] : '('.$terms[$term].')';
|
||||
if ($containsSpace) {
|
||||
$recomposed .= ")";
|
||||
$recomposed .= '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($terms['_default'] !== '') {
|
||||
$recomposed .= ' '.$terms['_default'];
|
||||
}
|
||||
|
||||
|
||||
//strip first character if empty
|
||||
if (mb_strcut($recomposed, 0, 1) === ' '){
|
||||
$recomposed = mb_strcut($recomposed, 1);
|
||||
}
|
||||
|
||||
|
||||
return $recomposed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,19 +20,15 @@ class SearchApi
|
||||
private EntityManagerInterface $em;
|
||||
private PaginatorFactory $paginator;
|
||||
|
||||
private array $providers = [];
|
||||
private iterable $providers = [];
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
SearchPersonApiProvider $searchPerson,
|
||||
ThirdPartyApiSearch $thirdPartyApiSearch,
|
||||
SearchUserApiProvider $searchUser,
|
||||
iterable $providers,
|
||||
PaginatorFactory $paginator
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->providers[] = $searchPerson;
|
||||
$this->providers[] = $thirdPartyApiSearch;
|
||||
$this->providers[] = $searchUser;
|
||||
$this->providers = $providers;
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
@ -68,10 +64,15 @@ class SearchApi
|
||||
|
||||
private function findProviders(string $pattern, array $types, array $parameters): array
|
||||
{
|
||||
return \array_filter(
|
||||
$this->providers,
|
||||
fn($p) => $p->supportsTypes($pattern, $types, $parameters)
|
||||
);
|
||||
$providers = [];
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider->supportsTypes($pattern, $types, $parameters)) {
|
||||
$providers[] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return $providers;
|
||||
}
|
||||
|
||||
private function countItems($providers, $types, $parameters): int
|
||||
@ -82,12 +83,12 @@ class SearchApi
|
||||
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
|
||||
$countNq->setParameters($parameters);
|
||||
|
||||
return $countNq->getSingleScalarResult();
|
||||
return (int) $countNq->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function buildCountQuery(array $queries, $types, $parameters)
|
||||
{
|
||||
$query = "SELECT COUNT(*) AS count FROM ({union_unordered}) AS sq";
|
||||
$query = "SELECT SUM(c) AS count FROM ({union_unordered}) AS sq";
|
||||
$unions = [];
|
||||
$parameters = [];
|
||||
|
||||
@ -141,17 +142,20 @@ class SearchApi
|
||||
private function prepareProviders(array $rawResults)
|
||||
{
|
||||
$metadatas = [];
|
||||
$providers = [];
|
||||
|
||||
foreach ($rawResults as $r) {
|
||||
foreach ($this->providers as $k => $p) {
|
||||
if ($p->supportsResult($r['key'], $r['metadata'])) {
|
||||
$metadatas[$k][] = $r['metadata'];
|
||||
$providers[$k] = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($metadatas as $k => $m) {
|
||||
$this->providers[$k]->prepare($m);
|
||||
$providers[$k]->prepare($m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ namespace Chill\MainBundle\Search;
|
||||
|
||||
class SearchApiQuery
|
||||
{
|
||||
private array $select = [];
|
||||
private array $selectParams = [];
|
||||
private ?string $selectKey = null;
|
||||
private array $selectKeyParams = [];
|
||||
private ?string $jsonbMetadata = null;
|
||||
@ -15,6 +17,38 @@ class SearchApiQuery
|
||||
private array $whereClauses = [];
|
||||
private array $whereClausesParams = [];
|
||||
|
||||
public function addSelectClause(string $select, array $params = []): self
|
||||
{
|
||||
$this->select[] = $select;
|
||||
$this->selectParams = [...$this->selectParams, ...$params];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetSelectClause(): self
|
||||
{
|
||||
$this->select = [];
|
||||
$this->selectParams = [];
|
||||
$this->selectKey = null;
|
||||
$this->selectKeyParams = [];
|
||||
$this->jsonbMetadata = null;
|
||||
$this->jsonbMetadataParams = [];
|
||||
$this->pertinence = null;
|
||||
$this->pertinenceParams = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectClauses(): array
|
||||
{
|
||||
return $this->select;
|
||||
}
|
||||
|
||||
public function getSelectParams(): array
|
||||
{
|
||||
return $this->selectParams;
|
||||
}
|
||||
|
||||
public function setSelectKey(string $selectKey, array $params = []): self
|
||||
{
|
||||
$this->selectKey = $selectKey;
|
||||
@ -47,6 +81,16 @@ class SearchApiQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFromClause(): string
|
||||
{
|
||||
return $this->fromClause;
|
||||
}
|
||||
|
||||
public function getFromParams(): array
|
||||
{
|
||||
return $this->fromClauseParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the where clause and replace all existing ones.
|
||||
*
|
||||
@ -54,7 +98,7 @@ class SearchApiQuery
|
||||
public function setWhereClauses(string $whereClause, array $params = []): self
|
||||
{
|
||||
$this->whereClauses = [$whereClause];
|
||||
$this->whereClausesParams = [$params];
|
||||
$this->whereClausesParams = $params;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -71,11 +115,53 @@ class SearchApiQuery
|
||||
public function andWhereClause(string $whereClause, array $params = []): self
|
||||
{
|
||||
$this->whereClauses[] = $whereClause;
|
||||
$this->whereClausesParams[] = $params;
|
||||
\array_push($this->whereClausesParams, ...$params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function buildSelectParams(bool $count = false): array
|
||||
{
|
||||
if ($count) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$args = $this->getSelectParams();
|
||||
|
||||
if (null !== $this->selectKey) {
|
||||
$args = [...$args, ...$this->selectKeyParams];
|
||||
}
|
||||
if (null !== $this->jsonbMetadata) {
|
||||
$args = [...$args, ...$this->jsonbMetadataParams];
|
||||
}
|
||||
if (null !== $this->pertinence) {
|
||||
$args = [...$args, ...$this->pertinenceParams];
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private function buildSelectClause(bool $countOnly = false): string
|
||||
{
|
||||
if ($countOnly) {
|
||||
return 'count(*) AS c';
|
||||
}
|
||||
|
||||
$selects = $this->getSelectClauses();
|
||||
|
||||
if (null !== $this->selectKey) {
|
||||
$selects[] = \strtr("'{key}' AS key", [ '{key}' => $this->selectKey ]);
|
||||
}
|
||||
if (null !== $this->jsonbMetadata) {
|
||||
$selects[] = \strtr('{metadata} AS metadata', [ '{metadata}' => $this->jsonbMetadata]);
|
||||
}
|
||||
if (null !== $this->pertinence) {
|
||||
$selects[] = \strtr('{pertinence} AS pertinence', [ '{pertinence}' => $this->pertinence]);
|
||||
}
|
||||
|
||||
return \implode(', ', $selects);
|
||||
}
|
||||
|
||||
public function buildQuery(bool $countOnly = false): string
|
||||
{
|
||||
$isMultiple = count($this->whereClauses);
|
||||
@ -87,19 +173,8 @@ class SearchApiQuery
|
||||
($isMultiple ? ')' : '')
|
||||
;
|
||||
|
||||
if (!$countOnly) {
|
||||
$select = \strtr("
|
||||
'{key}' AS key,
|
||||
{metadata} AS metadata,
|
||||
{pertinence} AS pertinence
|
||||
", [
|
||||
'{key}' => $this->selectKey,
|
||||
'{metadata}' => $this->jsonbMetadata,
|
||||
'{pertinence}' => $this->pertinence,
|
||||
]);
|
||||
} else {
|
||||
$select = "1 AS c";
|
||||
}
|
||||
$select = $this->buildSelectClause($countOnly);
|
||||
|
||||
|
||||
return \strtr("SELECT
|
||||
{select}
|
||||
@ -116,18 +191,16 @@ class SearchApiQuery
|
||||
public function buildParameters(bool $countOnly = false): array
|
||||
{
|
||||
if (!$countOnly) {
|
||||
return \array_merge(
|
||||
$this->selectKeyParams,
|
||||
$this->jsonbMetadataParams,
|
||||
$this->pertinenceParams,
|
||||
$this->fromClauseParams,
|
||||
\array_merge([], ...$this->whereClausesParams),
|
||||
);
|
||||
return [
|
||||
...$this->buildSelectParams($countOnly),
|
||||
...$this->fromClauseParams,
|
||||
...$this->whereClausesParams,
|
||||
];
|
||||
} else {
|
||||
return \array_merge(
|
||||
$this->fromClauseParams,
|
||||
\array_merge([], ...$this->whereClausesParams),
|
||||
);
|
||||
return [
|
||||
...$this->fromClauseParams,
|
||||
...$this->whereClausesParams,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
|
||||
* installed into the app.
|
||||
* the service is callable from the container with
|
||||
* $container->get('chill_main.search_provider')
|
||||
*
|
||||
* the syntax for search string is :
|
||||
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
|
||||
* entities. It may exists multiple search provider for the same domain (example:
|
||||
*
|
||||
* the syntax for search string is :
|
||||
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
|
||||
* entities. It may exists multiple search provider for the same domain (example:
|
||||
* a search provider for people which performs regular search, and suggestion search
|
||||
* with phonetical algorithms
|
||||
* - terms, which are the terms of the search. There are terms with argument (example :
|
||||
@ -25,17 +25,17 @@ class SearchProvider
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @var SearchInterface[]
|
||||
*/
|
||||
private $searchServices = array();
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @var HasAdvancedSearchForm[]
|
||||
*/
|
||||
private $hasAdvancedFormSearchServices = array();
|
||||
|
||||
|
||||
/*
|
||||
* return search services in an array, ordered by
|
||||
* the order key (defined in service definition)
|
||||
@ -59,7 +59,7 @@ class SearchProvider
|
||||
|
||||
return $this->searchServices;
|
||||
}
|
||||
|
||||
|
||||
public function getHasAdvancedFormSearchServices()
|
||||
{
|
||||
//sort the array
|
||||
@ -75,7 +75,7 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* parse the search string to extract domain and terms
|
||||
*
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return string[] an array where the keys are _domain, _default (residual terms) or term
|
||||
*/
|
||||
@ -95,9 +95,9 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* Extract the domain of the subject
|
||||
*
|
||||
*
|
||||
* The domain begins with `@`. Example: `@person`, `@report`, ....
|
||||
*
|
||||
*
|
||||
* @param type $subject
|
||||
* @return string
|
||||
* @throws ParsingException
|
||||
@ -121,14 +121,15 @@ class SearchProvider
|
||||
private function extractTerms(&$subject)
|
||||
{
|
||||
$terms = array();
|
||||
preg_match_all('/([a-z\-]+):([\w\-]+|\([^\(\r\n]+\))/', $subject, $matches);
|
||||
$matches = [];
|
||||
preg_match_all('/([a-z\-]+):(([^"][\S\-]+)|"[^"]*")/', $subject, $matches);
|
||||
|
||||
foreach ($matches[2] as $key => $match) {
|
||||
//remove from search pattern
|
||||
$this->mustBeExtracted[] = $matches[0][$key];
|
||||
//strip parenthesis
|
||||
if (mb_substr($match, 0, 1) === '(' &&
|
||||
mb_substr($match, mb_strlen($match) - 1) === ')') {
|
||||
if (mb_substr($match, 0, 1) === '"' &&
|
||||
mb_substr($match, mb_strlen($match) - 1) === '"') {
|
||||
$match = trim(mb_substr($match, 1, mb_strlen($match) - 2));
|
||||
}
|
||||
$terms[$matches[1][$key]] = $match;
|
||||
@ -139,14 +140,14 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* store string which must be extracted to find default arguments
|
||||
*
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $mustBeExtracted = array();
|
||||
|
||||
/**
|
||||
* extract default (residual) arguments
|
||||
*
|
||||
*
|
||||
* @param string $subject
|
||||
* @return string
|
||||
*/
|
||||
@ -158,7 +159,7 @@ class SearchProvider
|
||||
/**
|
||||
* search through services which supports domain and give
|
||||
* results as an array of resultsfrom different SearchInterface
|
||||
*
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param number $start
|
||||
* @param number $limit
|
||||
@ -167,25 +168,25 @@ class SearchProvider
|
||||
* @return array of results from different SearchInterface
|
||||
* @throws UnknowSearchDomainException if the domain is unknow
|
||||
*/
|
||||
public function getSearchResults($pattern, $start = 0, $limit = 50,
|
||||
public function getSearchResults($pattern, $start = 0, $limit = 50,
|
||||
array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->parse($pattern);
|
||||
$results = array();
|
||||
|
||||
|
||||
//sort searchServices by order
|
||||
$sortedSearchServices = array();
|
||||
foreach($this->searchServices as $service) {
|
||||
$sortedSearchServices[$service->getOrder()] = $service;
|
||||
}
|
||||
|
||||
|
||||
if ($terms['_domain'] !== NULL) {
|
||||
foreach ($sortedSearchServices as $service) {
|
||||
if ($service->supports($terms['_domain'], $format)) {
|
||||
$results[] = $service->renderResult($terms, $start, $limit, $options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (count($results) === 0) {
|
||||
throw new UnknowSearchDomainException($terms['_domain']);
|
||||
}
|
||||
@ -196,24 +197,24 @@ class SearchProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//sort array
|
||||
ksort($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
public function getResultByName($pattern, $name, $start = 0, $limit = 50,
|
||||
array $options = array(), $format = 'html')
|
||||
array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->parse($pattern);
|
||||
$search = $this->getByName($name);
|
||||
|
||||
|
||||
if ($terms['_domain'] !== NULL && !$search->supports($terms['_domain'], $format))
|
||||
{
|
||||
throw new ParsingException("The domain is not supported for the search name");
|
||||
}
|
||||
|
||||
|
||||
return $search->renderResult($terms, $start, $limit, $options, $format);
|
||||
}
|
||||
|
||||
@ -232,16 +233,16 @@ class SearchProvider
|
||||
throw new UnknowSearchNameException($name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return searchservice with an advanced form, defined in service
|
||||
* return searchservice with an advanced form, defined in service
|
||||
* definition.
|
||||
*
|
||||
*
|
||||
* @param string $name
|
||||
* @return HasAdvancedSearchForm
|
||||
* @throws UnknowSearchNameException
|
||||
*/
|
||||
public function getHasAdvancedFormByName($name)
|
||||
public function getHasAdvancedFormByName($name)
|
||||
{
|
||||
if (\array_key_exists($name, $this->hasAdvancedFormSearchServices)) {
|
||||
return $this->hasAdvancedFormSearchServices[$name];
|
||||
@ -253,7 +254,7 @@ class SearchProvider
|
||||
public function addSearchService(SearchInterface $service, $name)
|
||||
{
|
||||
$this->searchServices[$name] = $service;
|
||||
|
||||
|
||||
if ($service instanceof HasAdvancedSearchFormInterface) {
|
||||
$this->hasAdvancedFormSearchServices[$name] = $service;
|
||||
}
|
||||
@ -477,7 +478,7 @@ class SearchProvider
|
||||
$string = strtr($string, $chars);
|
||||
} /* remove from wordpress: we use only utf 8
|
||||
* else {
|
||||
|
||||
|
||||
// Assume ISO-8859-1 if not UTF-8
|
||||
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
|
||||
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
use \DateTimeImmutable;
|
||||
|
||||
class ExtractDateFromPattern
|
||||
{
|
||||
private const DATE_PATTERN = [
|
||||
["([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))", 'Y-m-d'], // 1981-05-12
|
||||
["((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))", 'd/m/Y'], // 15/12/1980
|
||||
["((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))", 'd-m-Y'], // 15/12/1980
|
||||
];
|
||||
|
||||
public function extractDates(string $subject): SearchExtractionResult
|
||||
{
|
||||
$dates = [];
|
||||
$filteredSubject = $subject;
|
||||
|
||||
foreach (self::DATE_PATTERN as [$pattern, $format]) {
|
||||
$matches = [];
|
||||
\preg_match_all($pattern, $filteredSubject, $matches);
|
||||
|
||||
foreach ($matches[0] as $match) {
|
||||
$date = DateTimeImmutable::createFromFormat($format, $match);
|
||||
if (false !== $date) {
|
||||
$dates[] = $date;
|
||||
// filter string: remove what is found
|
||||
$filteredSubject = \trim(\strtr($filteredSubject, [$match => ""]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchExtractionResult($filteredSubject, $dates);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
|
||||
class ExtractPhonenumberFromPattern
|
||||
{
|
||||
private const PATTERN = "([\+]{0,1}[0-9\ ]{5,})";
|
||||
|
||||
public function extractPhonenumber(string $subject): SearchExtractionResult
|
||||
{
|
||||
$matches = [];
|
||||
\preg_match(self::PATTERN, $subject,$matches);
|
||||
|
||||
if (0 < count($matches)) {
|
||||
$phonenumber = [];
|
||||
$length = 0;
|
||||
|
||||
foreach (\str_split(\trim($matches[0])) as $key => $char) {
|
||||
switch ($char) {
|
||||
case '0':
|
||||
$length++;
|
||||
if ($key === 0) { $phonenumber[] = '+32'; }
|
||||
else { $phonenumber[] = $char; }
|
||||
break;
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
$length++;
|
||||
$phonenumber[] = $char;
|
||||
break;
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException("should not match not alnum character");
|
||||
}
|
||||
}
|
||||
|
||||
if ($length > 5) {
|
||||
$filtered = \trim(\strtr($subject, [$matches[0] => '']));
|
||||
|
||||
return new SearchExtractionResult($filtered, [\implode('', $phonenumber)] );
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchExtractionResult($subject, []);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
class SearchExtractionResult
|
||||
{
|
||||
private string $filteredSubject;
|
||||
private array $found;
|
||||
|
||||
public function __construct(string $filteredSubject, array $found)
|
||||
{
|
||||
$this->filteredSubject = $filteredSubject;
|
||||
$this->found = $found;
|
||||
}
|
||||
|
||||
public function getFound(): array
|
||||
{
|
||||
return $this->found;
|
||||
}
|
||||
|
||||
public function hasResult(): bool
|
||||
{
|
||||
return [] !== $this->found;
|
||||
}
|
||||
|
||||
public function getFilteredSubject(): string
|
||||
{
|
||||
return $this->filteredSubject;
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ class SearchApiQueryTest extends TestCase
|
||||
$q = new SearchApiQuery();
|
||||
$q->setSelectJsonbMetadata('boum')
|
||||
->setSelectKey('bim')
|
||||
->setSelectPertinence('1')
|
||||
->setSelectPertinence('?', ['gamma'])
|
||||
->setFromClause('badaboum')
|
||||
->andWhereClause('foo', [ 'alpha' ])
|
||||
->andWhereClause('bar', [ 'beta' ])
|
||||
@ -21,12 +21,12 @@ class SearchApiQueryTest extends TestCase
|
||||
$query = $q->buildQuery();
|
||||
|
||||
$this->assertStringContainsString('(foo) AND (bar)', $query);
|
||||
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
|
||||
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
|
||||
|
||||
$query = $q->buildQuery(true);
|
||||
|
||||
$this->assertStringContainsString('(foo) AND (bar)', $query);
|
||||
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
|
||||
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
|
||||
}
|
||||
|
||||
public function testWithoutWhereClause()
|
||||
@ -42,4 +42,20 @@ class SearchApiQueryTest extends TestCase
|
||||
$this->assertEquals([], $q->buildParameters());
|
||||
}
|
||||
|
||||
public function testBuildParams()
|
||||
{
|
||||
$q = new SearchApiQuery();
|
||||
|
||||
$q
|
||||
->addSelectClause('bada', [ 'one', 'two' ])
|
||||
->addSelectClause('boum', ['three', 'four'])
|
||||
->setWhereClauses('mywhere', [ 'six', 'seven'])
|
||||
;
|
||||
|
||||
$params = $q->buildParameters();
|
||||
|
||||
$this->assertEquals(['six', 'seven'], $q->buildParameters(true));
|
||||
$this->assertEquals(['one', 'two', 'three', 'four', 'six', 'seven'], $q->buildParameters());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,17 +26,17 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SearchProviderTest extends TestCase
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @var SearchProvider
|
||||
* @var SearchProvider
|
||||
*/
|
||||
private $search;
|
||||
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->search = new SearchProvider();
|
||||
|
||||
|
||||
//add a default service
|
||||
$this->addSearchService(
|
||||
$this->createDefaultSearchService('I am default', 10), 'default'
|
||||
@ -46,7 +46,7 @@ class SearchProviderTest extends TestCase
|
||||
$this->createNonDefaultDomainSearchService('I am domain bar', 20, FALSE), 'bar'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\UnknowSearchNameException
|
||||
*/
|
||||
@ -54,11 +54,11 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
$this->search->getByName("invalid name");
|
||||
}
|
||||
|
||||
|
||||
public function testSimplePattern()
|
||||
{
|
||||
$terms = $this->p("@person birthdate:2014-01-02 name:(my name) is not my name");
|
||||
|
||||
$terms = $this->p("@person birthdate:2014-01-02 name:\"my name\" is not my name");
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'birthdate' => '2014-01-02',
|
||||
@ -66,40 +66,40 @@ class SearchProviderTest extends TestCase
|
||||
'name' => 'my name'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testWithoutDomain()
|
||||
{
|
||||
$terms = $this->p('foo:bar residual');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'foo' => 'bar',
|
||||
'_default' => 'residual'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testWithoutDefault()
|
||||
{
|
||||
$terms = $this->p('@person foo:bar');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'foo' => 'bar',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testCapitalLetters()
|
||||
{
|
||||
$terms = $this->p('Foo:Bar LOL marCi @PERSON');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'_default' => 'lol marci',
|
||||
'foo' => 'bar'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException Chill\MainBundle\Search\ParsingException
|
||||
*/
|
||||
@ -107,12 +107,11 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
$term = $this->p("@person @report");
|
||||
}
|
||||
|
||||
|
||||
public function testDoubleParenthesis()
|
||||
{
|
||||
$terms = $this->p("@papamobile name:(my beautiful name) residual "
|
||||
. "surname:(i love techno)");
|
||||
|
||||
$terms = $this->p('@papamobile name:"my beautiful name" residual surname:"i love techno"');
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'papamobile',
|
||||
'name' => 'my beautiful name',
|
||||
@ -120,65 +119,65 @@ class SearchProviderTest extends TestCase
|
||||
'surname' => 'i love techno'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testAccentued()
|
||||
{
|
||||
//$this->markTestSkipped('accentued characters must be implemented');
|
||||
|
||||
|
||||
$terms = $this->p('manço bélier aztèque à saloù ê');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => NULL,
|
||||
'_default' => 'manco belier azteque a salou e'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testAccentuedCapitals()
|
||||
{
|
||||
//$this->markTestSkipped('accentued characters must be implemented');
|
||||
|
||||
|
||||
$terms = $this->p('MANÉÀ oÛ lÎ À');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'_default' => 'manea ou li a'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testTrimInParenthesis()
|
||||
{
|
||||
$terms = $this->p('foo:(bar )');
|
||||
|
||||
$terms = $this->p('foo:"bar "');
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'foo' => 'bar',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testTrimInDefault()
|
||||
{
|
||||
$terms = $this->p(' foo bar ');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'_default' => 'foo bar'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testArgumentNameWithTrait()
|
||||
{
|
||||
$terms = $this->p('date-from:2016-05-04');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'date-from' => '2016-05-04',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the behaviour when no domain is provided in the search pattern :
|
||||
* Test the behaviour when no domain is provided in the search pattern :
|
||||
* the default search should be enabled
|
||||
*/
|
||||
public function testSearchResultDefault()
|
||||
@ -186,12 +185,12 @@ class SearchProviderTest extends TestCase
|
||||
$response = $this->search->getSearchResults('default search');
|
||||
|
||||
//$this->markTestSkipped();
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
"I am default"
|
||||
), $response);
|
||||
), $response);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\UnknowSearchDomainException
|
||||
*/
|
||||
@ -200,49 +199,49 @@ class SearchProviderTest extends TestCase
|
||||
$response = $this->search->getSearchResults('@unknow domain');
|
||||
|
||||
//$this->markTestSkipped();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testSearchResultDomainSearch()
|
||||
{
|
||||
//add a search service which will be supported
|
||||
$this->addSearchService(
|
||||
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
|
||||
);
|
||||
|
||||
|
||||
$response = $this->search->getSearchResults('@foo default search');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
"I am domain foo"
|
||||
), $response);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testSearchWithinSpecificSearchName()
|
||||
{
|
||||
//add a search service which will be supported
|
||||
$this->addSearchService(
|
||||
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
|
||||
);
|
||||
|
||||
|
||||
$response = $this->search->getResultByName('@foo search', 'foo');
|
||||
|
||||
|
||||
$this->assertEquals('I am domain foo', $response);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\ParsingException
|
||||
*/
|
||||
public function testSearchWithinSpecificSearchNameInConflictWithSupport()
|
||||
{
|
||||
$response = $this->search->getResultByName('@foo default search', 'bar');
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shortcut for executing parse method
|
||||
*
|
||||
*
|
||||
* @param unknown $pattern
|
||||
* @return string[]
|
||||
*/
|
||||
@ -250,12 +249,12 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
return $this->search->parse($pattern);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a search service to the chill.main.search_provider
|
||||
*
|
||||
*
|
||||
* Useful for mocking the SearchInterface
|
||||
*
|
||||
*
|
||||
* @param SearchInterface $search
|
||||
* @param string $name
|
||||
*/
|
||||
@ -264,52 +263,52 @@ class SearchProviderTest extends TestCase
|
||||
$this->search
|
||||
->addSearchService($search, $name);
|
||||
}
|
||||
|
||||
|
||||
private function createDefaultSearchService($result, $order)
|
||||
{
|
||||
$mock = $this
|
||||
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
|
||||
|
||||
|
||||
//set the mock as default
|
||||
$mock->expects($this->any())
|
||||
->method('isActiveByDefault')
|
||||
->will($this->returnValue(TRUE));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('getOrder')
|
||||
->will($this->returnValue($order));
|
||||
|
||||
|
||||
//set the return value
|
||||
$mock->expects($this->any())
|
||||
->method('renderResult')
|
||||
->will($this->returnValue($result));
|
||||
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
|
||||
private function createNonDefaultDomainSearchService($result, $order, $domain)
|
||||
{
|
||||
$mock = $this
|
||||
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
|
||||
|
||||
|
||||
//set the mock as default
|
||||
$mock->expects($this->any())
|
||||
->method('isActiveByDefault')
|
||||
->will($this->returnValue(FALSE));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('getOrder')
|
||||
->will($this->returnValue($order));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('supports')
|
||||
->will($this->returnValue($domain));
|
||||
|
||||
|
||||
//set the return value
|
||||
$mock->expects($this->any())
|
||||
->method('renderResult')
|
||||
->will($this->returnValue($result));
|
||||
|
||||
|
||||
return $mock;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Search\Utils;
|
||||
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExtractDateFromPatternTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* @dataProvider provideSubjects
|
||||
*/
|
||||
public function testExtractDate(string $subject, string $filtered, int $count, ...$datesSearched)
|
||||
{
|
||||
$extractor = new ExtractDateFromPattern();
|
||||
$result = $extractor->extractDates($subject);
|
||||
|
||||
$this->assertCount($count, $result->getFound());
|
||||
$this->assertEquals($filtered, $result->getFilteredSubject());
|
||||
$this->assertContainsOnlyInstancesOf(\DateTimeImmutable::class, $result->getFound());
|
||||
|
||||
$dates = \array_map(
|
||||
function (\DateTimeImmutable $d) {
|
||||
return $d->format('Y-m-d');
|
||||
}, $result->getFound()
|
||||
);
|
||||
|
||||
foreach ($datesSearched as $date) {
|
||||
$this->assertContains($date, $dates);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideSubjects()
|
||||
{
|
||||
yield ["15/06/1981", "", 1, '1981-06-15'];
|
||||
yield ["15/06/1981 30/12/1987", "", 2, '1981-06-15', '1987-12-30'];
|
||||
yield ["diallo 15/06/1981", "diallo", 1, '1981-06-15'];
|
||||
yield ["diallo 31/03/1981", "diallo", 1, '1981-03-31'];
|
||||
yield ["diallo 15-06-1981", "diallo", 1, '1981-06-15'];
|
||||
yield ["diallo 1981-12-08", "diallo", 1, '1981-12-08'];
|
||||
yield ["diallo", "diallo", 0];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Search\Utils;
|
||||
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
class ExtractPhonenumberFromPatternTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideData
|
||||
*/
|
||||
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg)
|
||||
{
|
||||
$extractor = new ExtractPhonenumberFromPattern();
|
||||
$result = $extractor->extractPhonenumber($subject);
|
||||
|
||||
$this->assertCount($expectedCount, $result->getFound());
|
||||
$this->assertEquals($filteredSubject, $result->getFilteredSubject());
|
||||
$this->assertEquals($expected, $result->getFound());
|
||||
}
|
||||
|
||||
public function provideData()
|
||||
{
|
||||
yield ['Diallo', 0, [], 'Diallo', "no phonenumber"];
|
||||
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', "no phonenumber and a date"];
|
||||
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', "a phonenumber and a name"];
|
||||
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', "a number and a name, without leadiing 0"];
|
||||
yield ['123 456', 1, ['123456'], '', "only phonenumber"];
|
||||
yield ['0123 456', 1, ['+32123456'], '', "only phonenumber with a leading 0"];
|
||||
}
|
||||
|
||||
}
|
@ -8,7 +8,16 @@ services:
|
||||
|
||||
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
|
||||
|
||||
Chill\MainBundle\Search\SearchApi: ~
|
||||
Chill\MainBundle\Search\SearchApi:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$providers: !tagged_iterator chill.search_api_provider
|
||||
|
||||
Chill\MainBundle\Search\Entity\:
|
||||
resource: '../../Search/Entity'
|
||||
|
||||
Chill\MainBundle\Search\Utils\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: './../Search/Utils/'
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20211119173554 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'users.attributes'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@ -86,6 +86,10 @@ address more:
|
||||
Create a new address: Créer une nouvelle adresse
|
||||
Create an address: Créer une adresse
|
||||
Update address: Modifier l'adresse
|
||||
City or postal code: Ville ou code postal
|
||||
|
||||
# contact
|
||||
Part of the phonenumber: Partie du numéro de téléphone
|
||||
|
||||
#serach
|
||||
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.
|
||||
@ -190,7 +194,7 @@ Location: Localisation
|
||||
Location type list: Liste des types de localisation
|
||||
Create a new location type: Créer un nouveau type de localisation
|
||||
Available for users: Disponible aux utilisateurs
|
||||
Address required: Adresse requise?
|
||||
Address required: Adresse requise?
|
||||
Contact data: Données de contact?
|
||||
optional: optionnel
|
||||
required: requis
|
||||
|
@ -86,13 +86,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
$loader->load('services/security.yaml');
|
||||
$loader->load('services/doctrineEventListener.yaml');
|
||||
|
||||
// load service advanced search only if configure
|
||||
if ($config['search']['search_by_phone'] != 'never') {
|
||||
$loader->load('services/search_by_phone.yaml');
|
||||
$container->setParameter('chill_person.search.search_by_phone',
|
||||
$config['search']['search_by_phone']);
|
||||
}
|
||||
|
||||
if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') {
|
||||
$loader->load('services/exports_accompanying_period.yaml');
|
||||
}
|
||||
|
@ -27,19 +27,6 @@ class Configuration implements ConfigurationInterface
|
||||
$rootNode
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
->arrayNode('search')
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
->enumNode('search_by_phone')
|
||||
->values(['always', 'on-domain', 'never'])
|
||||
->defaultValue('on-domain')
|
||||
->info('enable search by phone. \'always\' show the result '
|
||||
. 'on every result. \'on-domain\' will show the result '
|
||||
. 'only if the domain is given in the search box. '
|
||||
. '\'never\' disable this feature')
|
||||
->end()
|
||||
->end() //children for 'search', parent = array node 'search'
|
||||
->end() // array 'search', parent = children of root
|
||||
->arrayNode('validation')
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
|
@ -434,6 +434,5 @@ class Household
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
dump($cond);
|
||||
}
|
||||
}
|
||||
|
@ -39,10 +39,16 @@ use DateTimeInterface;
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_person_person",
|
||||
* indexes={@ORM\Index(
|
||||
* indexes={
|
||||
* @ORM\Index(
|
||||
* name="person_names",
|
||||
* columns={"firstName", "lastName"}
|
||||
* )})
|
||||
* ),
|
||||
* @ORM\Index(
|
||||
* name="person_birthdate",
|
||||
* columns={"birthdate"}
|
||||
* )
|
||||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "person"=Person::class
|
||||
|
@ -9,7 +9,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
* Person Phones
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_person_phone")
|
||||
* @ORM\Table(name="chill_person_phone",
|
||||
* indexes={
|
||||
* @ORM\Index(name="phonenumber", columns={"phonenumber"})
|
||||
* })
|
||||
*/
|
||||
class PersonPhone
|
||||
{
|
||||
@ -107,7 +110,7 @@ class PersonPhone
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->getDescription()) && empty($this->getPhonenumber());
|
||||
|
@ -31,7 +31,7 @@ class GenderType extends AbstractType {
|
||||
'choices' => $a,
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'placeholder' => null
|
||||
'placeholder' => null,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,15 @@
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApi;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
@ -49,125 +53,114 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city);
|
||||
|
||||
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the search query for PersonACL.
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter. It also add query limits.
|
||||
*
|
||||
* The given alias must represent the person alias.
|
||||
*
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
|
||||
{
|
||||
if ($simplify) {
|
||||
$qb->select(
|
||||
$alias.'.id',
|
||||
$qb->expr()->concat(
|
||||
$alias.'.firstName',
|
||||
$qb->expr()->literal(' '),
|
||||
$alias.'.lastName'
|
||||
).'AS text'
|
||||
);
|
||||
} else {
|
||||
$qb->select($alias);
|
||||
}
|
||||
|
||||
$qb
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($start);
|
||||
|
||||
//order by firstname, lastname
|
||||
$qb
|
||||
->orderBy($alias.'.firstName')
|
||||
->addOrderBy($alias.'.lastName');
|
||||
|
||||
if ($simplify) {
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
||||
} else {
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
return $this->fetchQueryPerson($query);
|
||||
}
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): int {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city)
|
||||
;
|
||||
|
||||
return $this->getCountQueryResult($qb,'p');
|
||||
return $this->fetchQueryCount($query);
|
||||
}
|
||||
|
||||
public function fetchQueryCount(SearchApiQuery $query): int
|
||||
{
|
||||
$rsm = new Query\ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c');
|
||||
|
||||
$nql = $this->em->createNativeQuery($query->buildQuery(true), $rsm);
|
||||
$nql->setParameters($query->buildParameters(true));
|
||||
|
||||
return $nql->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the count for search query
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter.
|
||||
*
|
||||
* The given alias must represent the person alias in the query builder.
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getCountQueryResult(QueryBuilder $qb, $alias): int
|
||||
public function fetchQueryPerson(SearchApiQuery $query, ?int $start = 0, ?int $limit = 50): array
|
||||
{
|
||||
$qb->select('COUNT('.$alias.'.id)');
|
||||
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||
$rsm->addRootEntityFromClassMetadata(Person::class, 'person');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
$query->addSelectClause($rsm->generateSelectClause());
|
||||
|
||||
$nql = $this->em->createNativeQuery(
|
||||
$query->buildQuery()." ORDER BY pertinence DESC OFFSET ? LIMIT ?", $rsm
|
||||
)->setParameters(\array_merge($query->buildParameters(), [$start, $limit]));
|
||||
|
||||
return $nql->getResult();
|
||||
}
|
||||
|
||||
public function findBySimilaritySearch(string $pattern, int $firstResult,
|
||||
int $maxResult, bool $simplify = false)
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode, $phonenumber)
|
||||
;
|
||||
|
||||
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
|
||||
return $this->addAuthorizations($query);
|
||||
}
|
||||
|
||||
public function countBySimilaritySearch(string $pattern)
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
return $this->getCountQueryResult($qb, 'sp');
|
||||
if ([] === $authorizedCenters) {
|
||||
return $query->andWhereClause("FALSE = TRUE", []);
|
||||
}
|
||||
|
||||
return $query
|
||||
->andWhereClause(
|
||||
strtr(
|
||||
"person.center_id IN ({{ center_ids }})",
|
||||
[
|
||||
'{{ center_ids }}' => \implode(', ',
|
||||
\array_fill(0, count($authorizedCenters), '?')),
|
||||
]
|
||||
),
|
||||
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query without ACL
|
||||
*
|
||||
* The person alias is a "p"
|
||||
*
|
||||
* @param string|null $default
|
||||
* @param string|null $firstname
|
||||
* @param string|null $lastname
|
||||
* @param \DateTime|null $birthdate
|
||||
* @param \DateTime|null $birthdateBefore
|
||||
* @param \DateTime|null $birthdateAfter
|
||||
* @param string|null $gender
|
||||
* @param string|null $countryCode
|
||||
* @return QueryBuilder
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
@ -175,118 +168,107 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): QueryBuilder {
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
->setFromClause("chill_person_person AS person")
|
||||
;
|
||||
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
throw new \RuntimeException("Search must be performed by a valid user");
|
||||
}
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->from(Person::class, 'p');
|
||||
$pertinence = [];
|
||||
$pertinenceArgs = [];
|
||||
$orWhereSearchClause = [];
|
||||
$orWhereSearchClauseArgs = [];
|
||||
|
||||
if (NULL !== $firstname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
|
||||
->setParameter('firstname', '%'.$firstname.'%');
|
||||
}
|
||||
if ("" !== $default) {
|
||||
foreach (\explode(" ", $default) as $str) {
|
||||
$pertinence[] =
|
||||
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
|
||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
|
||||
\array_push($pertinenceArgs, $str, $str, $str);
|
||||
|
||||
if (NULL !== $lastname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
|
||||
->setParameter('lastname', '%'.$lastname.'%');
|
||||
$orWhereSearchClause[] =
|
||||
"(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
|
||||
\array_push($orWhereSearchClauseArgs, $str, $str);
|
||||
}
|
||||
|
||||
$query->andWhereClause(\implode(' OR ', $orWhereSearchClause),
|
||||
$orWhereSearchClauseArgs);
|
||||
} else {
|
||||
$pertinence = ["1"];
|
||||
$pertinenceArgs = [];
|
||||
}
|
||||
$query
|
||||
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs)
|
||||
;
|
||||
|
||||
if (NULL !== $birthdate) {
|
||||
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
|
||||
->setParameter('birthdate', $birthdate);
|
||||
$query->andWhereClause(
|
||||
"person.birthdate = ?::date",
|
||||
[$birthdate->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
|
||||
->setParameter('birthdateafter', $birthdateAfter);
|
||||
if (NULL !== $firstname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.firstname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$firstname]
|
||||
);
|
||||
}
|
||||
if (NULL !== $lastname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.lastname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$lastname]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateBefore) {
|
||||
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
|
||||
->setParameter('birthdatebefore', $birthdateBefore);
|
||||
$query->andWhereClause(
|
||||
'p.birthdate < ?::date',
|
||||
[$birthdateBefore->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $gender) {
|
||||
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
|
||||
->setParameter('gender', $gender);
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$query->andWhereClause(
|
||||
'p.birthdate > ?::date',
|
||||
[$birthdateAfter->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $countryCode) {
|
||||
try {
|
||||
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
|
||||
} catch (NoResultException $ex) {
|
||||
throw new ParsingException('The country code "'.$countryCode.'" '
|
||||
. ', used in nationality, is unknow', 0, $ex);
|
||||
} catch (NonUniqueResultException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
|
||||
->setParameter('nationality', $country);
|
||||
if (NULL !== $phonenumber) {
|
||||
$query->andWhereClause(
|
||||
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'"
|
||||
,
|
||||
[$phonenumber, $phonenumber, $phonenumber]
|
||||
);
|
||||
$query->setFromClause($query->getFromClause()." LEFT JOIN chill_person_phone pp ON pp.person_id = person.id");
|
||||
}
|
||||
if (null !== $city) {
|
||||
$query->setFromClause($query->getFromClause()." ".
|
||||
"JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id ".
|
||||
"JOIN chill_main_address cma ON vcpca.address_id = cma.id ".
|
||||
"JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id");
|
||||
|
||||
if (NULL !== $default) {
|
||||
$grams = explode(' ', $default);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere($qb->expr()
|
||||
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
foreach (\explode(" ", $city) as $cityStr) {
|
||||
$query->andWhereClause(
|
||||
"(UNACCENT(LOWER(cmpc.label)) LIKE '%' || UNACCENT(LOWER(?)) || '%' OR cmpc.code LIKE '%' || UNACCENT(LOWER(?)) || '%')",
|
||||
[$cityStr, $city]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
|
||||
{
|
||||
// restrict center for security
|
||||
$reachableCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()
|
||||
->in($personAlias.'.center', ':centers'),
|
||||
$qb->expr()
|
||||
->isNull($personAlias.'.center')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('centers', $reachableCenters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for searching by similarity.
|
||||
*
|
||||
* The person alias is "sp".
|
||||
*
|
||||
* @param $pattern
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function createSimilarityQuery($pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb->from(Person::class, 'sp');
|
||||
|
||||
$grams = explode(' ', $pattern);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere('STRICT_WORD_SIMILARITY_OPS(:default_'.$key.', sp.fullnameCanonical) = TRUE')
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
|
||||
// remove the perfect matches
|
||||
$qb->andWhere($qb->expr()
|
||||
->notLike('sp.fullnameCanonical', 'UNACCENT(LOWER(:not_default_'.$key.'))'))
|
||||
->setParameter('not_default_'.$key, '%'.$gram.'%');
|
||||
if (null !== $countryCode) {
|
||||
$query->setFromClause($query->getFromClause()." JOIN country ON person.nationality_id = country.id");
|
||||
$query->andWhereClause("country.countrycode = UPPER(?)", [$countryCode]);
|
||||
}
|
||||
if (null !== $gender) {
|
||||
$query->andWhereClause("person.gender = ?", [$gender]);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,14 @@
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
|
||||
interface PersonACLAwareRepositoryInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @return array|Person[]
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public function findBySearchCriteria(
|
||||
int $start,
|
||||
@ -21,30 +19,38 @@ interface PersonACLAwareRepositoryInterface
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array;
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
);
|
||||
|
||||
public function findBySimilaritySearch(
|
||||
string $pattern,
|
||||
int $firstResult,
|
||||
int $maxResult,
|
||||
bool $simplify = false
|
||||
);
|
||||
|
||||
public function countBySimilaritySearch(string $pattern);
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery;
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
{% macro button_person(person) %}
|
||||
{% macro button_person_after(person) %}
|
||||
{% set household = person.getCurrentHousehold %}
|
||||
{% if household is not null %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}"
|
||||
class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
|
||||
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}" class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
@ -56,7 +61,7 @@
|
||||
'addAltNames': true,
|
||||
'addCenter': true,
|
||||
'address_multiline': false,
|
||||
'customButtons': { 'after': _self.button_person(person) }
|
||||
'customButtons': { 'after': _self.button_person_after(person) }
|
||||
}) }}
|
||||
|
||||
{#- 'acps' is for AcCompanyingPeriodS #}
|
||||
@ -76,12 +81,20 @@
|
||||
<div class="wl-row separator">
|
||||
<div class="wl-col title">
|
||||
|
||||
<div class="date">
|
||||
{% if acp.requestorPerson == person %}
|
||||
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
|
||||
{% if acp.step == 'DRAFT' %}
|
||||
<div class="is-draft">
|
||||
<span class="course-draft badge bg-secondary" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if acp.requestorPerson == person %}
|
||||
<div>
|
||||
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
|
||||
{{ 'Requestor'|trans({'gender': person.gender}) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="date">
|
||||
{% if app != null %}
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
{% endif %}
|
||||
@ -94,6 +107,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="courseid">
|
||||
{{ 'File number'|trans }} {{ acp.id }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
|
||||
@ -101,17 +119,76 @@
|
||||
{{ issue|chill_entity_render_box }}
|
||||
{% endfor %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions record_actions_column">
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if acp.currentParticipations|length > 1 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<div class="participants">
|
||||
{{ 'Participants'|trans }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% set participating = false %}
|
||||
{% for part in acp.currentParticipations %}
|
||||
{% if part.person.id != person.id %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: part.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: part.person|chill_entity_render_string
|
||||
} %}
|
||||
{% else %}
|
||||
{% set participating = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if participating %}
|
||||
{{ 'person.and_himself'|trans({'gender': person.gender}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<div>
|
||||
{% if acp.requestorPerson is not null %}
|
||||
{{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }}
|
||||
{% else %}
|
||||
{{ 'Requestor'|trans({'gender': 'other'})}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if acp.requestorThirdParty is not null %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'thirdparty', id: acp.requestorThirdParty.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: acp.requestorThirdParty|chill_entity_render_string
|
||||
} %}
|
||||
{% else %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: acp.requestorPerson.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: acp.requestorPerson|chill_entity_render_string
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,11 +5,15 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\PersonBundle\Form\Type\GenderType;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
@ -19,23 +23,29 @@ use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
|
||||
{
|
||||
protected EngineInterface $templating;
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private EngineInterface $templating;
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public const NAME = "person_regular";
|
||||
|
||||
private const POSSIBLE_KEYS = [
|
||||
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
|
||||
'birthdate-after', 'gender', 'nationality'
|
||||
'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
EngineInterface $templating,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository
|
||||
) {
|
||||
$this->templating = $templating;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
}
|
||||
@ -69,6 +79,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
*/
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->findAdditionnalInDefault($terms);
|
||||
$total = $this->count($terms);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
@ -99,6 +110,26 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
}
|
||||
}
|
||||
|
||||
private function findAdditionnalInDefault(array $terms): array
|
||||
{
|
||||
// chaining some extractor
|
||||
$datesResults = $this->extractDateFromPattern->extractDates($terms['_default']);
|
||||
$phoneResults = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResults->getFilteredSubject());
|
||||
$terms['_default'] = $phoneResults->getFilteredSubject();
|
||||
|
||||
if ($datesResults->hasResult() && (!\array_key_exists('birthdate', $terms)
|
||||
|| NULL !== $terms['birthdate'])) {
|
||||
$terms['birthdate'] = $datesResults->getFound()[0]->format('Y-m-d');
|
||||
}
|
||||
|
||||
if ($phoneResults->hasResult() && (!\array_key_exists('phonenumber', $terms)
|
||||
|| NULL !== $terms['phonenumber'])) {
|
||||
$terms['phonenumber'] = $phoneResults->getFound()[0];
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Person[]
|
||||
*/
|
||||
@ -113,6 +144,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@ -139,6 +172,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@ -153,7 +188,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@ -177,6 +213,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@ -207,13 +245,19 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'label' => 'Birthdate before',
|
||||
'required' => false
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'Man' => Person::MALE_GENDER,
|
||||
'Woman' => Person::FEMALE_GENDER
|
||||
],
|
||||
->add('phonenumber', TelType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Part of the phonenumber'
|
||||
])
|
||||
->add('gender', GenderType::class, [
|
||||
'label' => 'Gender',
|
||||
'required' => false
|
||||
'required' => false,
|
||||
'expanded' => false,
|
||||
'placeholder' => 'All genders'
|
||||
])
|
||||
->add('city', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'City or postal code'
|
||||
])
|
||||
;
|
||||
}
|
||||
@ -224,7 +268,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
|
||||
$string .= empty($data['_default']) ? '' : $data['_default'].' ';
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', 'phonenumber', 'city'] as $key) {
|
||||
$string .= empty($data[$key]) ? '' : $key.':'.
|
||||
// add quote if contains spaces
|
||||
(strpos($data[$key], ' ') !== false ? '"'.$data[$key].'"': $data[$key])
|
||||
@ -246,7 +290,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender', '_default'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', '_default', 'phonenumber', 'city'] as $key) {
|
||||
$data[$key] = $terms[$key] ?? null;
|
||||
}
|
||||
|
||||
@ -275,6 +319,4 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class PersonSearchByPhone extends AbstractSearch
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PersonRepository
|
||||
*/
|
||||
private $personRepository;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
private $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginatorFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $activeByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Templating
|
||||
*/
|
||||
protected $engine;
|
||||
|
||||
const NAME = 'phone';
|
||||
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $helper,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
EngineInterface $engine,
|
||||
$activeByDefault)
|
||||
{
|
||||
$this->personRepository = $personRepository;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->helper = $helper;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->engine = $engine;
|
||||
$this->activeByDefault = $activeByDefault === 'always';
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 110;
|
||||
}
|
||||
|
||||
public function isActiveByDefault(): bool
|
||||
{
|
||||
return $this->activeByDefault;
|
||||
}
|
||||
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, $options = array(), $format = 'html')
|
||||
{
|
||||
$phonenumber = $terms['_default'];
|
||||
$centers = $this->helper->getReachableCenters($this->tokenStorage
|
||||
->getToken()->getUser(), new Role(PersonVoter::SEE));
|
||||
$total = $this->personRepository
|
||||
->countByPhone($phonenumber, $centers);
|
||||
$persons = $this->personRepository
|
||||
->findByPhone($phonenumber, $centers, $start, $limit)
|
||||
;
|
||||
$paginator = $this->paginatorFactory
|
||||
->create($total);
|
||||
|
||||
return $this->engine->render('ChillPersonBundle:Person:list_by_phonenumber.html.twig',
|
||||
array(
|
||||
'persons' => $persons,
|
||||
'pattern' => $this->recomposePattern($terms, array(), $terms['_domain'] ?? self::NAME),
|
||||
'phonenumber' => $phonenumber,
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
|
||||
'paginator' => $paginator
|
||||
));
|
||||
}
|
||||
|
||||
public function supports($domain, $format): bool
|
||||
{
|
||||
return $domain === 'phone' && $format === 'html';
|
||||
}
|
||||
}
|
@ -2,12 +2,13 @@
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class SearchPersonApiProvider implements SearchApiInterface
|
||||
@ -15,59 +16,47 @@ class SearchPersonApiProvider implements SearchApiInterface
|
||||
private PersonRepository $personRepository;
|
||||
private Security $security;
|
||||
private AuthorizationHelperInterface $authorizationHelper;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public function __construct(PersonRepository $personRepository, Security $security, AuthorizationHelperInterface $authorizationHelper)
|
||||
{
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
Security $security,
|
||||
AuthorizationHelperInterface $authorizationHelper,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern
|
||||
) {
|
||||
$this->personRepository = $personRepository;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->security = $security;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
}
|
||||
|
||||
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
return $this->addAuthorizations($this->buildBaseQuery($pattern, $parameters));
|
||||
}
|
||||
$datesResult = $this->extractDateFromPattern->extractDates($pattern);
|
||||
$phoneResult = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResult->getFilteredSubject());
|
||||
$filtered = $phoneResult->getFilteredSubject();
|
||||
|
||||
public function buildBaseQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
return $this->personACLAwareRepository->buildAuthorizedQuery(
|
||||
$filtered,
|
||||
null,
|
||||
null,
|
||||
count($datesResult->getFound()) > 0 ? $datesResult->getFound()[0] : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
count($phoneResult->getFound()) > 0 ? $phoneResult->getFound()[0] : null
|
||||
)
|
||||
->setSelectKey("person")
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
|
||||
->setSelectPertinence("".
|
||||
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
|
||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"
|
||||
, [ $pattern, $pattern, $pattern ])
|
||||
->setFromClause("chill_person_person AS person")
|
||||
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
|
||||
;
|
||||
|
||||
return $query;
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)");
|
||||
}
|
||||
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
if ([] === $authorizedCenters) {
|
||||
return $query->andWhereClause("FALSE = TRUE", []);
|
||||
}
|
||||
|
||||
return $query
|
||||
->andWhereClause(
|
||||
strtr(
|
||||
"person.center_id IN ({{ center_ids }})",
|
||||
[
|
||||
'{{ center_ids }}' => \implode(', ',
|
||||
\array_fill(0, count($authorizedCenters), '?')),
|
||||
]
|
||||
),
|
||||
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsTypes(string $pattern, array $types, array $parameters): bool
|
||||
{
|
||||
|
@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
|
||||
private EngineInterface $templating;
|
||||
|
||||
const NAME = "person_similarity";
|
||||
|
||||
public function __construct(
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
EngineInterface $templating
|
||||
) {
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->templating = $templating;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
|
||||
*/
|
||||
public function isActiveByDefault()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supports($domain, $format)
|
||||
{
|
||||
return 'person' === $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $terms
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param array $options
|
||||
* @param string $format
|
||||
*/
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
|
||||
{
|
||||
$total = $this->count($terms);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
if ($format === 'html') {
|
||||
if ($total !== 0) {
|
||||
return $this->templating->render('ChillPersonBundle:Person:list.html.twig',
|
||||
array(
|
||||
'persons' => $this->search($terms, $start, $limit, $options),
|
||||
'pattern' => $this->recomposePattern($terms, array('nationality',
|
||||
'firstname', 'lastname', 'birthdate', 'gender',
|
||||
'birthdate-before','birthdate-after'), $terms['_domain']),
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
|
||||
'paginator' => $paginator,
|
||||
'title' => "Similar persons"
|
||||
));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} elseif ($format === 'json') {
|
||||
return [
|
||||
'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])),
|
||||
'pagination' => [
|
||||
'more' => $paginator->hasNextPage()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param array $options
|
||||
* @return Person[]
|
||||
*/
|
||||
protected function search(array $terms, $start, $limit, array $options = array())
|
||||
{
|
||||
return $this->personACLAwareRepository
|
||||
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
|
||||
}
|
||||
|
||||
protected function count(array $terms)
|
||||
{
|
||||
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
|
||||
}
|
||||
}
|
@ -4,11 +4,6 @@ services:
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_regular' }
|
||||
|
||||
Chill\PersonBundle\Search\SimilarityPersonSearch:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_similarity' }
|
||||
|
||||
Chill\PersonBundle\Search\SimilarPersonMatcher:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@ -1,11 +0,0 @@
|
||||
services:
|
||||
Chill\PersonBundle\Search\PersonSearchByPhone:
|
||||
arguments:
|
||||
- '@Chill\PersonBundle\Repository\PersonRepository'
|
||||
- '@security.token_storage'
|
||||
- '@chill.main.security.authorization.helper'
|
||||
- '@chill_main.paginator_factory'
|
||||
- '@Symfony\Component\Templating\EngineInterface'
|
||||
- '%chill_person.search.search_by_phone%'
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_by_phone' }
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119211101 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'add indexes for searching by birthdate';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE INDEX person_birthdate ON chill_person_person (birthdate)');
|
||||
$this->addSql('CREATE INDEX phonenumber ON chill_person_phone USING GIST (phonenumber gist_trgm_ops)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX person_birthdate');
|
||||
$this->addSql('DROP INDEX phonenumber');
|
||||
}
|
||||
}
|
@ -12,6 +12,14 @@ Requestor: >-
|
||||
other {Demandeur·euse}
|
||||
}
|
||||
|
||||
person:
|
||||
and_himself: >-
|
||||
{gender, select,
|
||||
man {et lui-même}
|
||||
woman {et elle-même}
|
||||
other {et lui·elle-même}
|
||||
}
|
||||
|
||||
household:
|
||||
Household: Ménage
|
||||
Household number: Ménage {household_num}
|
||||
|
@ -84,6 +84,7 @@ Married: Marié(e)
|
||||
File number: Dossier n°
|
||||
Civility: Civilité
|
||||
choose civility: --
|
||||
All genders: tous les genres
|
||||
|
||||
# dédoublonnage
|
||||
Old person: Doublon
|
||||
@ -207,6 +208,7 @@ Referrer: Référent
|
||||
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible.
|
||||
Add to household now: Ajouter à un ménage
|
||||
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
|
||||
course.draft: Brouillon
|
||||
|
||||
# pickAPersonType
|
||||
Pick a person: Choisir une personne
|
||||
|
Loading…
x
Reference in New Issue
Block a user