Merge remote-tracking branch 'origin/master' into issue296_internal_close_accourse

This commit is contained in:
2021-11-22 17:14:11 +01:00
118 changed files with 3939 additions and 1367 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace Chill\PersonBundle\DataFixtures\Helper;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
trait RandomPersonHelperTrait
{
private ?int $nbOfPersons = null;
protected function getRandomPerson(EntityManagerInterface $em): Person
{
$qb = $em->createQueryBuilder();
$qb
->from(Person::class, 'p')
;
if (null === $this->nbOfPersons) {
$this->nbOfPersons = $qb
->select('COUNT(p)')
->getQuery()
->getSingleScalarResult()
;
}
return $qb
->select('p')
->setMaxResults(1)
->setFirstResult(\random_int(0, $this->nbOfPersons))
->getQuery()
->getSingleResult()
;
}
}

View File

@@ -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');
}

View File

@@ -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()

View File

@@ -1150,11 +1150,11 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
public function getGroupSequence()
{
if ($this->getStep() == self::STEP_DRAFT) {
if ($this->getStep() == self::STEP_DRAFT)
{
return [[self::STEP_DRAFT]];
}
if ($this->getStep() == self::STEP_CONFIRMED) {
} elseif ($this->getStep() == self::STEP_CONFIRMED)
{
return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
}

View File

@@ -434,6 +434,5 @@ class Household
->addViolation();
}
}
dump($cond);
}
}

View File

@@ -37,20 +37,20 @@ class MaritalStatus
* @ORM\Id()
* @ORM\Column(type="string", length=7)
*/
private $id;
private ?string $id;
/**
* @var string array
* @ORM\Column(type="json")
*/
private $name;
private array $name;
/**
* Get id
*
* @return string
*/
public function getId()
public function getId(): string
{
return $this->id;
}
@@ -61,7 +61,7 @@ class MaritalStatus
* @param string $id
* @return MaritalStatus
*/
public function setId($id)
public function setId(string $id): self
{
$this->id = $id;
return $this;
@@ -73,7 +73,7 @@ class MaritalStatus
* @param string array $name
* @return MaritalStatus
*/
public function setName($name)
public function setName(array $name): self
{
$this->name = $name;
@@ -85,7 +85,7 @@ class MaritalStatus
*
* @return string array
*/
public function getName()
public function getName(): array
{
return $this->name;
}

View File

@@ -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
@@ -213,7 +219,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private ?\DateTime $maritalStatusDate;
private ?\DateTime $maritalStatusDate = null;
/**
* Comment on marital status
@@ -246,7 +252,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* The person's phonenumber
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @ORM\Column(type="text")
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
@@ -256,13 +262,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private $phonenumber = '';
private string $phonenumber = '';
/**
* The person's mobile phone number
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @ORM\Column(type="text")
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
@@ -272,7 +278,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private $mobilenumber = '';
private string $mobilenumber = '';
/**
* @var Collection
@@ -1088,9 +1094,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/**
* Get nationality
*
* @return Chill\MainBundle\Entity\Country
* @return Country
*/
public function getNationality()
public function getNationality(): ?Country
{
return $this->nationality;
}
@@ -1170,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*
* @return string
*/
public function getPhonenumber()
public function getPhonenumber(): string
{
return $this->phonenumber;
}
@@ -1193,7 +1199,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*
* @return string
*/
public function getMobilenumber()
public function getMobilenumber(): string
{
return $this->mobilenumber;
}

View File

@@ -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());

View File

@@ -31,7 +31,7 @@ class GenderType extends AbstractType {
'choices' => $a,
'expanded' => true,
'multiple' => false,
'placeholder' => null
'placeholder' => null,
));
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -60,6 +60,16 @@ h2.badge-title {
h3 {
margin-bottom: 0.5rem;
}
//position: relative;
span {
display: none;
//position: absolute;
//top: 0;
//left: 0;
//transform: rotate(270deg);
//transform-origin: 0 0;
}
}
span.title_action {
flex-grow: 1;
@@ -117,3 +127,36 @@ div.activity-list {
}
}
}
/// AccompanyingCourse: HeaderSlider Carousel
div#header-accompanying_course-details {
button.carousel-control-prev,
button.carousel-control-next {
width: 8%;
opacity: inherit;
}
button.carousel-control-prev {
left: unset;
right: 0;
}
span.to-social-issues,
span.to-persons-associated {
display: inline-block;
border-radius: 15px;
width: 24px;
height: 24px;
box-shadow: 0 0 3px 1px grey;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
span.to-social-issues {
background-color: #4bafe8;
border-left: 12px solid #32749a;
}
span.to-persons-associated {
background-color: #16d9b4;
border-right: 12px solid #ffffff;
}
}

View File

@@ -4,7 +4,7 @@
///
@mixin chill_badge($color) {
text-transform: capitalize !important;
//text-transform: capitalize !important;
font-weight: 500 !important;
border-left: 20px groove $color;
&:before {

View File

@@ -22,7 +22,7 @@
<i>{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}</i>
</span>
<span v-if="accompanyingCourse.user" class="d-md-block ms-3 ms-md-0">
<abbr :title="$t('course.referrer')">{{ $t('course.referrer') }}:</abbr> <b>{{ accompanyingCourse.user.username }}</b>
<span class="item-key">{{ $t('course.referrer') }}:</span> <b>{{ accompanyingCourse.user.username }}</b>
</span>
</span>
</span>
@@ -34,13 +34,15 @@
</teleport>
<teleport to="#header-accompanying_course-details #banner-social-issues">
<div class="col-12">
<social-issue
v-for="issue in accompanyingCourse.socialIssues"
v-bind:key="issue.id"
v-bind:issue="issue">
</social-issue>
</div>
<social-issue
v-for="issue in accompanyingCourse.socialIssues"
v-bind:key="issue.id"
v-bind:issue="issue">
</social-issue>
</teleport>
<teleport to="#header-accompanying_course-details #banner-persons-associated">
<persons-associated :accompanyingCourse="accompanyingCourse"></persons-associated>
</teleport>
</template>
@@ -48,12 +50,14 @@
<script>
import ToggleFlags from './Banner/ToggleFlags';
import SocialIssue from './Banner/SocialIssue.vue';
import PersonsAssociated from './Banner/PersonsAssociated.vue';
export default {
name: 'Banner',
components: {
ToggleFlags,
SocialIssue
SocialIssue,
PersonsAssociated
},
computed: {
accompanyingCourse() {

View File

@@ -0,0 +1,75 @@
<template>
<span v-for="h in personsByHousehold()" :class="{ 'household': householdExists(h.id), 'no-household': !householdExists(h.id) }">
<a v-if="householdExists(h.id)" :href="householdLink(h.id)">
<i class="fa fa-home fa-fw text-light" :title="$t('persons_associated.show_household_number', { id: h.id })"></i>
</a>
<span v-for="person in h.persons" class="me-1">
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.text" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
</span>
</span>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly'
export default {
name: "PersonsAssociated",
components: {
OnTheFly
},
props: [ 'accompanyingCourse' ],
computed: {
participations() {
return this.accompanyingCourse.participations.filter(p => p.endDate === null)
},
persons() {
return this.participations.map(p => p.person)
},
resources() {
return this.accompanyingCourse.resources
},
requestor() {
return this.accompanyingCourse.requestor
}
},
methods: {
uniq(array) {
return [...new Set(array)]
},
personsByHousehold() {
let households = []
this.persons.forEach(p => { households.push(p.current_household_id) })
let personsByHousehold = []
this.uniq(households).forEach(h => {
personsByHousehold.push({
id: h !== null ? h : 0,
persons: this.persons.filter(p => p.current_household_id === h)
})
})
console.log(personsByHousehold)
return personsByHousehold
},
householdExists(id) {
return id !== 0
},
householdLink(id) {
return `/fr/person/household/${id}/summary`
}
}
}
</script>
<style lang="scss" scoped>
span.household {
display: inline-block;
border-top: 1px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin-right: 0.3em;
padding: 5px;
}
</style>

View File

@@ -10,7 +10,7 @@
<div class="alert alert-warning">
{{ $t('confirm.alert_validation') }}
<ul class="mt-2">
<li v-for="k in validationKeys">
<li v-for="k in validationKeys" :key=k>
{{ $t(notValidMessages[k].msg) }}
<a :href="notValidMessages[k].anchor">
<i class="fa fa-level-up fa-fw"></i>
@@ -83,7 +83,11 @@ export default {
},
location: {
msg: 'confirm.location_not_valid',
anchor: '#section-20' //
anchor: '#section-20'
},
origin: {
msg: 'confirm.origin_not_valid',
anchor: '#section-30'
},
socialIssue: {
msg: 'confirm.socialIssue_not_valid',
@@ -103,6 +107,7 @@ export default {
...mapGetters([
'isParticipationValid',
'isSocialIssueValid',
'isOriginValid',
'isLocationValid',
'validationKeys',
'isValidToBeConfirmed'

View File

@@ -19,15 +19,18 @@
:options="options"
@select="updateOrigin">
</VueMultiselect>
</div>
<div v-if="!isOriginValid" class="alert alert-warning to-confirm">
{{ $t('origin.not_valid') }}
</div>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
import { getListOrigins } from '../api';
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
export default {
name: 'OriginDemand',
@@ -41,6 +44,9 @@ export default {
...mapState({
value: state => state.accompanyingCourse.origin,
}),
...mapGetters([
'isOriginValid'
])
},
mounted() {
this.getOptions();

View File

@@ -5,7 +5,7 @@
addId : false,
addEntity: false,
addLink: false,
addHouseholdLink: true,
addHouseholdLink: false,
addAltNames: true,
addAge : true,
hLevel : 3,
@@ -20,14 +20,15 @@
v-if="hasCurrentHouseholdAddress"
v-bind:person="participation.person">
</button-location>
<li v-if="participation.person.current_household_id">
<a class="btn btn-sm btn-chill-beige"
:href="getCurrentHouseholdUrl"
:title="$t('persons_associated.show_household_number', { id: participation.person.current_household_id })">
<i class="fa fa-fw fa-home"></i>
</a>
</li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<!-- <li>
<button class="btn btn-delete"
:title="$t('action.delete')"
@click.prevent="$emit('remove', participation)">
</button>
</li> -->
<li>
<button v-if="!participation.endDate"
class="btn btn-sm btn-remove"
@@ -100,6 +101,9 @@ export default {
},
getAccompanyingCourseReturnPath() {
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`;
},
getCurrentHouseholdUrl() {
return `/fr/person/household/${this.participation.person.current_household_id}/summary?returnPath=${this.getAccompanyingCourseReturnPath}`
}
},
methods: {

View File

@@ -19,16 +19,16 @@
@select="updateReferrer">
</VueMultiselect>
<template v-if="referrersSuggested.length > 0">
<ul>
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
<user-render-box-badge :user="u"></user-render-box-badge>
</li>
<ul class="list-unstyled">
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
<span class="badge bg-primary" style="cursor: pointer">
<i class="fa fa-plus fa-fw text-success"></i>
<user-render-box-badge :user="u"></user-render-box-badge>
</span>
</li>
</ul>
</template>
</div>
<div>

View File

@@ -35,6 +35,7 @@ const appMessages = {
title: "Origine de la demande",
label: "Origine de la demande",
placeholder: "Renseignez l'origine de la demande",
not_valid: "Indiquez une origine de la demande",
},
persons_associated: {
title: "Usagers concernés",
@@ -53,7 +54,7 @@ const appMessages = {
show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage",
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
update_household: "Modifier l'appartenance",
update_household: "Renseigner l'appartenance",
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
},
requestor: {
@@ -125,6 +126,7 @@ const appMessages = {
participation_not_valid: "sélectionnez au minimum 1 usager",
socialIssue_not_valid: "sélectionnez au minimum une problématique sociale",
location_not_valid: "indiquez au minimum une localisation temporaire du parcours",
origin_not_valid: "indiquez une origine de la demande",
set_a_scope: "indiquez au moins un service",
sure: "Êtes-vous sûr ?",
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",

View File

@@ -52,6 +52,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
isSocialIssueValid(state) {
return state.accompanyingCourse.socialIssues.length > 0;
},
isOriginValid(state) {
return state.accompanyingCourse.origin !== null;
},
isLocationValid(state) {
return state.accompanyingCourse.location !== null;
},
@@ -64,6 +67,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
if (!getters.isParticipationValid) { keys.push('participation'); }
if (!getters.isLocationValid) { keys.push('location'); }
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
if (!getters.isOriginValid) { keys.push('origin'); }
if (!getters.isScopeValid) { keys.push('scopes'); }
//console.log('getter keys', keys);
return keys;

View File

@@ -119,9 +119,9 @@
<div id="persons" class="action-row">
<h3>{{ $t('persons_involved') }}</h3>
<ul>
<ul class="list-unstyled">
<li v-for="p in personsReachables" :key="p.id">
<input v-model="personsPicked" :value="p.id" type="checkbox">
<input v-model="personsPicked" :value="p.id" type="checkbox" class="me-2">
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
</li>
</ul>

View File

@@ -96,14 +96,9 @@
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.center && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-if="person.center.type !== undefined">
{{ person.center.name }}
</template>
<template v-else>
<template v-for="c in person.center">{{ c.name }}</template>
</template>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>

View File

@@ -6,7 +6,7 @@
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-10' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
{{ 'fix it'|trans }}
</a>
</li>
</ul>

View File

@@ -8,7 +8,7 @@
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
{{ 'fix it'|trans }}
</a>
</li>
</ul>

View File

@@ -23,11 +23,28 @@
</div>
<div id="header-accompanying_course-details" class="header-details">
<div class="container-xxl">
<div
class="row justify-content-md-right">
<div class="row">
{# vue teleport fragment here #}
<div class="col-md-10 ps-md-5 ps-xxl-0" id="banner-social-issues"></div>
<div class="col-md-12 px-md-5 px-xxl-0">
<div id="ACHeaderSlider" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
{# vue teleport fragment here #}
<div id="banner-social-issues" class="col-11"></div>
</div>
<div class="carousel-item">
{# vue teleport fragment here #}
<div id="banner-persons-associated" class="col-11"></div>
</div>
</div>
<button class="carousel-control-prev justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="prev">
<span class="to-social-issues" title="{{ 'see social issues'|trans }}"></span>
</button>
<button class="carousel-control-next justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="next">
<span class="to-persons-associated" title="{{ 'see persons associated'|trans }}"></span>
</button>
</div>
</div>
</div>
</div>

View File

@@ -23,32 +23,6 @@
{% block content %}
<div class="accompanyingcourse-resume row">
<div class="associated-persons mb-5">
{% for h in participationsByHousehold %}
{% set householdClass = (h.household is not null) ? 'household-' ~ h.household.id : 'no-household alert alert-warning' %}
{% set householdTitle = (h.household is not null) ?
'household.Household number'|trans({'household_num': h.household.id }) : 'household.Never in any household'|trans %}
<span class="household {{ householdClass }}" title="{{ householdTitle }}">
{% if h.household is not null %}
<a href="{{ path('chill_person_household_summary', { 'household_id': h.household.id }) }}"
title="{{ 'household.Household number'|trans({'household_num': h.household.id }) }}"
><i class="fa fa-home fa-fw"></i></a>
{% endif %}
{% for p in h.members %}
{# include vue_onthefly component #}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: p.person.id },
action: 'show',
displayBadge: true,
buttonText: p.person|chill_entity_render_string
} %}
{% endfor %}
</span>
{% endfor %}
</div>
{% if 'DRAFT' == accompanyingCourse.step %}
<div class="col-md-6 warnings mb-5">
{% include '@ChillPerson/AccompanyingCourse/_still_draft.html.twig' %}
@@ -83,7 +57,7 @@
</div>
<div class="social-actions mb-5">
<h2 class="mb-3">{{ 'Last social actions'|trans }}</h2>
<h2 class="mb-3 d-none">{{ 'Last social actions'|trans }}</h2>
{% include 'ChillPersonBundle:AccompanyingCourseWork:list_recent_by_accompanying_period.html.twig' with {'buttonText': false } %}
</div>
@@ -101,8 +75,7 @@
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
<h2 class="mb-3">{{ 'Last activities' |trans }}</h2>
<h2 class="mb-3 d-none">{{ 'Last activities' |trans }}</h2>
{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
</div>
{% endblock %}

View File

@@ -1,29 +1,33 @@
{% if works|length == 0 %}
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}
<a class="btn btn-sm btn-create"
href="" title="TODO"></a>{# TODO link #}
</p>
{% endif %}
<div class="accompanying_course_work-list">
{% for w in works | slice(0,5) %}
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"></a>
<h2 class="badge-title">
<span class="title_label">{{ 'accompanying_course_work.action'|trans }}</span>
<span class="title_action">{{ w.socialAction|chill_entity_render_string }}
<span class="title_label">
<span>{{ 'accompanying_course_work.action'|trans }}</span>
</span>
<span class="title_action">
{{ w.socialAction|chill_entity_render_string }}
<ul class="small_in_title">
<li>
<abbr title="{{ 'accompanying_course_work.start_date'|trans }}">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</abbr>
{{ w.startDate|format_date('short') }}
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ w.startDate|format_date('short') }}</b>
</li>
{% if w.endDate %}
<li>
<abbr title="{{ 'Last updated by'|trans }}">{{ 'Last updated by'|trans ~ ' : ' }}</abbr>
{{ w.updatedBy|chill_entity_render_box }}, {{ w.updatedAt|format_datetime('short', 'short') }}
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ w.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
<div class="metadata text-end" style="font-size: 60%">
{{ 'Last updated by'|trans }}
<span class="user">{{ w.updatedBy|chill_entity_render_box }}</span>:
<span class="date">{{ w.updatedAt|format_datetime('short', 'short') }}</span>
</div>
</span>
</h2>
{% endfor %}

View File

@@ -148,10 +148,12 @@
{% endif %}
{% endif %}
</li>
{% if options['addCenter'] and person|chill_resolve_center is not null %}
{% if options['addCenter'] and person|chill_resolve_center|length > 0 %}
<li>
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person|chill_resolve_center.name }}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
</li>
{% endif %}
</ul>

View File

@@ -17,18 +17,14 @@
{%- endif -%}
</div>
<div class="text-md-end">
{% if person|chill_resolve_center is not null %}
{% if person|chill_resolve_center|length > 0 %}
<span class="open_sansbold">
{{ 'Center'|trans|upper}} :
</span>
{% if person|chill_resolve_center is iterable %}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
{{ person|chill_resolve_center.name|upper }}
{% endif %}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
{%- endif -%}
</div>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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';
}
}

View File

@@ -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
{

View File

@@ -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']);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PersonDocGenNormalizer implements
ContextAwareNormalizerInterface,
NormalizerAwareInterface
{
use NormalizerAwareTrait;
private PersonRender $personRender;
private TranslatorInterface $translator;
private TranslatableStringHelper $translatableStringHelper;
/**
* @param PersonRender $personRender
* @param TranslatorInterface $translator
* @param TranslatableStringHelper $translatableStringHelper
*/
public function __construct(
PersonRender $personRender,
TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper
) {
$this->personRender = $personRender;
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
}
public function normalize($person, string $format = null, array $context = [])
{
/** @var Person $person */
$dateContext = $context;
$dateContext['docgen:expects'] = \DateTimeInterface::class;
if (null === $person) {
return $this->normalizeNullValue($format, $context);
}
return [
'firstname' => $person->getFirstName(),
'lastname' => $person->getLastName(),
'altNames' => \implode(
', ',
\array_map(
function (PersonAltName $altName) {
return $altName->getLabel();
},
$person->getAltNames()->toArray()
)
),
'text' => $this->personRender->renderString($person, []),
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext),
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext),
'gender' => $this->translator->trans($person->getGender()),
'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
'email' => $person->getEmail(),
'firstPhoneNumber' => $person->getPhonenumber() ?? $person->getMobilenumber(),
'fixPhoneNumber' => $person->getPhonenumber(),
'mobilePhoneNumber' => $person->getMobilenumber(),
'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '',
'placeOfBirth' => $person->getPlaceOfBirth(),
'memo' => $person->getMemo(),
'numberOfChildren' => (string) $person->getNumberOfChildren(),
];
}
private function normalizeNullValue(string $format, array $context)
{
$normalizer = new NormalizeNullValueHelper($this->normalizer);
$attributes = [
'firstname', 'lastname', 'altNames', 'text',
'birthdate' => \DateTimeInterface::class,
'deathdate' => \DateTimeInterface::class,
'gender', 'maritalStatus',
'maritalStatusDate' => \DateTimeInterface::class,
'email', 'firstPhoneNumber', 'fixPhoneNumber', 'mobilePhoneNumber', 'nationality',
'placeOfBirth', 'memo', 'numberOfChildren'
];
return $normalizer->normalize($attributes, $format, $context);
}
public function supportsNormalization($data, string $format = null, array $context = [])
{
if ($format !== 'docgen') {
return false;
}
return
$data instanceof Person
|| (
\array_key_exists('docgen:expects', $context)
&& $context['docgen:expects'] === Person::class
);
}
}

View File

@@ -20,6 +20,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
@@ -39,7 +40,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
* Serialize a Person entity
*
*/
class PersonNormalizer implements
class PersonJsonNormalizer implements
NormalizerInterface,
NormalizerAwareInterface,
DenormalizerInterface,
@@ -49,7 +50,7 @@ class PersonNormalizer implements
private PersonRepository $repository;
private CenterResolverDispatcher $centerResolverDispatcher;
private CenterResolverManagerInterface $centerResolverManager;
use NormalizerAwareTrait;
@@ -60,11 +61,11 @@ class PersonNormalizer implements
public function __construct(
ChillEntityRenderExtension $render,
PersonRepository $repository,
CenterResolverDispatcher $centerResolverDispatcher
CenterResolverManagerInterface $centerResolverManager
) {
$this->render = $render;
$this->repository = $repository;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->centerResolverManager = $centerResolverManager;
}
public function normalize($person, string $format = null, array $context = [])
@@ -79,15 +80,15 @@ class PersonNormalizer implements
'text' => $this->render->renderString($person),
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
'deathdate' => $this->normalizer->normalize($person->getDeathdate()),
'center' => $this->normalizer->normalize($this->centerResolverDispatcher->resolveCenter($person)),
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $context),
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $context),
'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context),
'phonenumber' => $person->getPhonenumber(),
'mobilenumber' => $person->getMobilenumber(),
'altNames' => $this->normalizeAltNames($person->getAltNames()),
'gender' => $person->getGender(),
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress()),
'current_household_id' => $household ? $this->normalizer->normalize($household->getId()) : null,
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress(), $format, $context),
'current_household_id' => $household ? $this->normalizer->normalize($household->getId(), $format, $context) : null,
];
}
@@ -105,7 +106,7 @@ class PersonNormalizer implements
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof Person;
return $data instanceof Person && $format === 'json';
}
public function denormalize($data, string $type, string $format = null, array $context = [])
@@ -128,45 +129,48 @@ class PersonNormalizer implements
$person = new Person();
}
$properties = ['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender'];
foreach (['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender',
'birthdate', 'deathdate', 'center']
as $item) {
$properties = array_filter(
$properties,
static fn (string $property): bool => array_key_exists($property, $data)
);
foreach ($properties as $item) {
$callable = [$person, sprintf('set%s', ucfirst($item))];
if (is_callable($callable)) {
$closure = \Closure::fromCallable($callable);
$closure($data[$item]);
if (!\array_key_exists($item, $data)) {
continue;
}
}
$propertyToClassMapping = [
'birthdate' => \DateTime::class,
'deathdate' => \DateTime::class,
'center' => Center::class,
];
$propertyToClassMapping = array_filter(
$propertyToClassMapping,
static fn (string $item): bool => array_key_exists($item, $data)
);
foreach ($propertyToClassMapping as $item => $class) {
$object = $this->denormalizer->denormalize($data[$item], $class, $format, $context);
if ($object instanceof $class) {
$callable = [$object, sprintf('set%s', ucfirst($item))];
if (is_callable($callable)) {
$closure = \Closure::fromCallable($callable);
$closure($object);
}
switch ($item) {
case 'firstName':
$person->setFirstName($data[$item]);
break;
case 'lastName':
$person->setLastName($data[$item]);
break;
case 'phonenumber':
$person->setPhonenumber($data[$item]);
break;
case 'mobilenumber':
$person->setMobilenumber($data[$item]);
break;
case 'gender':
$person->setGender($data[$item]);
break;
case 'birthdate':
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
if ($object instanceof \DateTime) {
$person->setBirthdate($object);
}
break;
case 'deathdate':
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
if ($object instanceof \DateTime) {
$person->setDeathdate($object);
}
break;
case 'center':
$object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
$person->setCenter($object);
break;
default:
throw new \LogicException("item not defined: $item");
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Serializer\Normalizer\PersonDocGenNormalizer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PersonDocGenNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp()
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
/**
* @dataProvider generateData
*/
public function testNormalize(?Person $person, $expected, $msg)
{
$normalized = $this->normalizer->normalize($person, 'docgen', ['docgen:expects' => Person::class]);
$this->assertEquals($expected, $normalized, $msg);
}
public function generateData()
{
$person = new Person();
$person
->setFirstName('Renaud')
->setLastName('Mégane')
;
$expected = \array_merge(
self::BLANK, ['firstname' => 'Renaud', 'lastname' => 'Mégane',
'text' => 'Renaud Mégane']
);
yield [$person, $expected, 'partial normalization for a person'];
yield [null, self::BLANK, 'normalization for a null person'];
}
private const BLANK = [
'firstname' => '',
'lastname' => '',
'altNames' => '',
'text' => '',
'birthdate' => ['short' => '', 'long' => ''],
'deathdate' => ['short' => '', 'long' => ''],
'gender' => '',
'maritalStatus' => '',
'maritalStatusDate' => ['short' => '', 'long' => ''],
'email' => '',
'firstPhoneNumber' => '',
'fixPhoneNumber' => '',
'mobilePhoneNumber' => '',
'nationality' => '',
'placeOfBirth' => '',
'memo' => '',
'numberOfChildren' => ''
];
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PersonJsonNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp()
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$person = new Person();
$result = $this->normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => [ 'read' ]]);
$this->assertIsArray($result);
}
}

View File

@@ -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

View File

@@ -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' }

View File

@@ -4,6 +4,7 @@ services:
Chill\PersonBundle\Serializer\Normalizer\:
autowire: true
autoconfigure: true
resource: '../../Serializer/Normalizer'
tags:
- { name: 'serializer.normalizer', priority: 64 }

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* drop not null in person phonenumber
*/
final class Version20211112170027 extends AbstractMigration
{
public function getDescription(): string
{
return 'Drop not null in person table: set default empty value';
}
public function up(Schema $schema): void
{
$this->addSql('UPDATE chill_person_person SET mobilenumber = \'\' WHERE mobilenumber IS NULL');
$this->addSql('UPDATE chill_person_person SET phonenumber = \'\' WHERE phonenumber IS NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET NOT NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET NOT NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT \'\'');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT \'\'');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber DROP NOT NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber DROP NOT NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT NULL');
}
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20211119215630 extends AbstractMigration
{
public function getDescription(): string
{
return 'update computation of view_chill_person_current_address: do not take enddate into account';
}
public function up(Schema $schema): void
{
$this->addSql("CREATE OR REPLACE VIEW view_chill_person_current_address AS
SELECT
cphm.person_id AS person_id,
cma.id AS address_id,
CASE WHEN cphm.startdate > COALESCE(cma.validfrom, '-infinity'::date) THEN cphm.startdate ELSE cma.validfrom END AS valid_from,
CASE WHEN COALESCE(cphm.enddate, 'infinity'::date) < COALESCE(cma.validto, 'infinity'::date) THEN cphm.enddate ELSE cma.validto END AS valid_to
FROM chill_person_household_members AS cphm
JOIN chill_person_household_to_addresses AS cphta ON cphta.household_id = cphm.household_id
JOIN chill_main_address AS cma ON cphta.address_id = cma.id
WHERE
cphm.sharedhousehold IS TRUE
AND
daterange(cphm.startdate, cphm.enddate, '[)') @> current_date
AND
daterange(cma.validfrom, cma.validto, '[)') @> current_date
");
}
public function down(Schema $schema): void
{
$this->addSql("CREATE OR REPLACE VIEW view_chill_person_current_address AS
SELECT
cphm.person_id AS person_id,
cma.id AS address_id,
CASE WHEN cphm.startdate > COALESCE(cma.validfrom, '-infinity'::date) THEN cphm.startdate ELSE cma.validfrom END AS valid_from,
CASE WHEN COALESCE(cphm.enddate, 'infinity'::date) < COALESCE(cma.validto, 'infinity'::date) THEN cphm.enddate ELSE cma.validto END AS valid_to
FROM chill_person_household_members AS cphm
LEFT JOIN chill_person_household_to_addresses AS cphta ON cphta.household_id = cphm.household_id
LEFT JOIN chill_main_address AS cma ON cphta.address_id = cma.id
WHERE
cphm.sharedhousehold IS TRUE
AND
current_date between cphm.startdate AND coalesce(enddate, 'infinity'::date)
AND
current_date between cma.validfrom AND coalesce(validto, 'infinity'::date)
");
}
}

View File

@@ -12,11 +12,19 @@ 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}
Household members: Membres du ménage
Household editor: Modifier l'appartenance
Household editor: Renseigner l'appartenance
Members at same time: Membres simultanés
Any simultaneous members: Aucun membre simultanément
Select people to move: Choisir les usagers
@@ -51,7 +59,7 @@ household:
is holder: Est titulaire
is not holder: N'est pas titulaire
holder: Titulaire
Edit member household: Modifier l'appartenance au ménage
Edit member household: Renseigner l'appartenance au ménage
Edit his household: Modifier son appartenance au ménage
Current household members: Membres actuels
Household summary: Résumé du ménage
@@ -61,7 +69,7 @@ household:
Household relationships: Filiations dans le ménage
Current address: Adresse actuelle
Household does not have any address currently: Le ménage n'a pas d'adresse renseignée actuellement
Edit household members: Modifier l'appartenance au ménage
Edit household members: Renseigner l'appartenance au ménage
and x other persons: >-
{x, plural,
one {et une autre personne}

View File

@@ -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
@@ -392,6 +394,7 @@ This course has a temporarily location: Localisation temporaire
Choose a person to locate by: Localiser auprès d'un usager concerné
Associate at least one member with an household, and set an address to this household: Associez au moins un membre du parcours à un ménage, et indiquez une adresse à ce ménage.
Locate by: Localiser auprès de
fix it: Compléter
# Household
Household: Ménage
@@ -431,3 +434,5 @@ accompanying_course_work:
Person addresses: Adresses de résidence
Household addresses: Adresses de domicile
Insert an address: Insérer une adresse
see social issues: Voir les problématiques sociales
see persons associated: Voir les usagers concernés