mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-20 14:43:49 +00:00
Merge branch 'master' into person_renderbox_thirdparty_onthefly
This commit is contained in:
@@ -73,7 +73,7 @@ class AccompanyingCourseController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $period);
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
|
||||
|
||||
$em->persist($period);
|
||||
$em->flush();
|
||||
@@ -92,6 +92,8 @@ class AccompanyingCourseController extends Controller
|
||||
*/
|
||||
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
|
||||
|
||||
// compute some warnings
|
||||
// get persons without household
|
||||
$withoutHousehold = [];
|
||||
@@ -131,6 +133,8 @@ class AccompanyingCourseController extends Controller
|
||||
*/
|
||||
public function editAction(AccompanyingPeriod $accompanyingCourse): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
|
||||
|
||||
return $this->render('@ChillPerson/AccompanyingCourse/edit.html.twig', [
|
||||
'accompanyingCourse' => $accompanyingCourse
|
||||
]);
|
||||
@@ -146,6 +150,8 @@ class AccompanyingCourseController extends Controller
|
||||
*/
|
||||
public function historyAction(AccompanyingPeriod $accompanyingCourse): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
|
||||
|
||||
return $this->render('@ChillPerson/AccompanyingCourse/history.html.twig', [
|
||||
'accompanyingCourse' => $accompanyingCourse
|
||||
]);
|
||||
|
@@ -23,7 +23,10 @@
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\AccompanyingPeriodType;
|
||||
@@ -53,21 +56,24 @@ class AccompanyingPeriodController extends AbstractController
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* AccompanyingPeriodController constructor.
|
||||
*
|
||||
* @param EventDispatcherInterface $eventDispatcher
|
||||
* @param ValidatorInterface $validator
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, ValidatorInterface $validator)
|
||||
{
|
||||
protected AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
|
||||
|
||||
public function __construct(
|
||||
AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function listAction(int $person_id): Response
|
||||
/**
|
||||
* @ParamConverter("person", options={"id"="person_id"})
|
||||
*/
|
||||
public function listAction(Person $person): Response
|
||||
{
|
||||
$person = $this->_getPerson($person_id);
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $person);
|
||||
|
||||
$event = new PrivacyEvent($person, [
|
||||
'element_class' => AccompanyingPeriod::class,
|
||||
@@ -75,9 +81,10 @@ class AccompanyingPeriodController extends AbstractController
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
|
||||
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
|
||||
$accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository
|
||||
->findByPerson($person, AccompanyingPeriodVoter::SEE);
|
||||
|
||||
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
|
||||
return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [
|
||||
'accompanying_periods' => $accompanyingPeriods,
|
||||
'person' => $person
|
||||
]);
|
||||
|
@@ -230,13 +230,16 @@ final class PersonController extends AbstractController
|
||||
*/
|
||||
public function newAction(Request $request)
|
||||
{
|
||||
$defaultCenter = $this->security
|
||||
->getUser()
|
||||
->getGroupCenters()[0]
|
||||
->getCenter();
|
||||
$person = new Person();
|
||||
|
||||
$person = (new Person(new \DateTime('now')))
|
||||
->setCenter($defaultCenter);
|
||||
if (1 === count($this->security->getUser()
|
||||
->getGroupCenters())) {
|
||||
$person->setCenter(
|
||||
$this->security->getUser()
|
||||
->getGroupCenters()[0]
|
||||
->getCenter()
|
||||
);
|
||||
}
|
||||
|
||||
$form = $this->createForm(CreationPersonType::class, $person, [
|
||||
'validation_groups' => ['create']
|
||||
|
@@ -24,8 +24,12 @@ namespace Chill\PersonBundle\DataFixtures\ORM;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Country;
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CenterRepository;
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Repository\ScopeRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\MaritalStatus;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||
@@ -90,12 +94,26 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
|
||||
protected MaritalStatusRepository $maritalStatusRepository;
|
||||
|
||||
/**
|
||||
* @var array|Scope[]
|
||||
*/
|
||||
protected array $cacheScopes = [];
|
||||
|
||||
protected ScopeRepository $scopeRepository;
|
||||
|
||||
/** @var array|User[] */
|
||||
protected array $cacheUsers = [];
|
||||
|
||||
protected UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
Registry $workflowRegistry,
|
||||
SocialIssueRepository $socialIssueRepository,
|
||||
CenterRepository $centerRepository,
|
||||
CountryRepository $countryRepository,
|
||||
MaritalStatusRepository $maritalStatusRepository
|
||||
MaritalStatusRepository $maritalStatusRepository,
|
||||
ScopeRepository $scopeRepository,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->faker = Factory::create('fr_FR');
|
||||
$this->faker->addProvider($this);
|
||||
@@ -105,7 +123,8 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
$this->countryRepository = $countryRepository;
|
||||
$this->maritalStatusRepository = $maritalStatusRepository;
|
||||
$this->loader = new NativeLoader($this->faker);
|
||||
|
||||
$this->scopeRepository = $scopeRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
@@ -220,10 +239,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
new \DateInterval('P' . \random_int(0, 180) . 'D')
|
||||
)
|
||||
);
|
||||
$accompanyingPeriod->setCreatedBy($this->getRandomUser())
|
||||
->setCreatedAt(new \DateTimeImmutable('now'));
|
||||
$person->addAccompanyingPeriod($accompanyingPeriod);
|
||||
$accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue());
|
||||
|
||||
if (\random_int(0, 10) > 3) {
|
||||
// always add social scope:
|
||||
$accompanyingPeriod->addScope($this->getReference('scope_social'));
|
||||
var_dump(count($accompanyingPeriod->getScopes()));
|
||||
|
||||
$accompanyingPeriod->setAddressLocation($this->createAddress());
|
||||
$manager->persist($accompanyingPeriod->getAddressLocation());
|
||||
$workflow = $this->workflowRegistry->get($accompanyingPeriod);
|
||||
@@ -231,9 +256,19 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
}
|
||||
|
||||
$manager->persist($person);
|
||||
$manager->persist($accompanyingPeriod);
|
||||
echo "add person'".$person->__toString()."'\n";
|
||||
}
|
||||
|
||||
private function getRandomUser(): User
|
||||
{
|
||||
if (0 === count($this->cacheUsers)) {
|
||||
$this->cacheUsers = $this->userRepository->findAll();
|
||||
}
|
||||
|
||||
return $this->cacheUsers[\array_rand($this->cacheUsers)];
|
||||
}
|
||||
|
||||
private function createAddress(): Address
|
||||
{
|
||||
$objectSet = $this->loader->loadData([
|
||||
|
@@ -40,13 +40,13 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
return 9600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef);
|
||||
$scopeSocial = $this->getReference('scope_social');
|
||||
|
||||
|
||||
//create permission group
|
||||
switch ($permissionsGroup->getName()) {
|
||||
case 'social':
|
||||
@@ -55,7 +55,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
$permissionsGroup->addRoleScope(
|
||||
(new RoleScope())
|
||||
->setRole(AccompanyingPeriodVoter::SEE)
|
||||
->setRole(AccompanyingPeriodVoter::FULL)
|
||||
->setScope($scopeSocial)
|
||||
);
|
||||
|
||||
@@ -87,7 +87,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
$manager->persist($roleScopeUpdate);
|
||||
$manager->persist($roleScopeCreate);
|
||||
$manager->persist($roleScopeDuplicate);
|
||||
|
||||
|
||||
break;
|
||||
case 'administrative':
|
||||
printf("Adding CHILL_PERSON_SEE to %s permission group \n", $permissionsGroup->getName());
|
||||
@@ -98,9 +98,9 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
$manager->persist($roleScopeSee);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@
|
||||
|
||||
namespace Chill\PersonBundle\DependencyInjection;
|
||||
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
@@ -60,6 +61,9 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
$container->setParameter('chill_person.allow_multiple_simultaneous_accompanying_periods',
|
||||
$config['allow_multiple_simultaneous_accompanying_periods']);
|
||||
|
||||
// register all configuration in a unique parameter
|
||||
$container->setParameter('chill_person', $config);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/widgets.yaml');
|
||||
@@ -255,14 +259,26 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
*/
|
||||
protected function prependRoleHierarchy(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('security', array(
|
||||
'role_hierarchy' => array(
|
||||
'CHILL_PERSON_UPDATE' => array('CHILL_PERSON_SEE'),
|
||||
'CHILL_PERSON_CREATE' => array('CHILL_PERSON_SEE'),
|
||||
PersonVoter::LISTS => [ ChillExportVoter::EXPORT ],
|
||||
PersonVoter::STATS => [ ChillExportVoter::EXPORT ]
|
||||
)
|
||||
));
|
||||
$container->prependExtensionConfig('security', [
|
||||
'role_hierarchy' => [
|
||||
PersonVoter::UPDATE => [PersonVoter::SEE],
|
||||
PersonVoter::CREATE => [PersonVoter::SEE],
|
||||
PersonVoter::LISTS => [ChillExportVoter::EXPORT],
|
||||
PersonVoter::STATS => [ChillExportVoter::EXPORT],
|
||||
// accompanying period
|
||||
AccompanyingPeriodVoter::SEE_DETAILS => [AccompanyingPeriodVoter::SEE],
|
||||
AccompanyingPeriodVoter::CREATE => [AccompanyingPeriodVoter::SEE_DETAILS],
|
||||
AccompanyingPeriodVoter::DELETE => [AccompanyingPeriodVoter::SEE_DETAILS],
|
||||
AccompanyingPeriodVoter::EDIT => [AccompanyingPeriodVoter::SEE_DETAILS],
|
||||
// give all ACL for FULL
|
||||
AccompanyingPeriodVoter::FULL => [
|
||||
AccompanyingPeriodVoter::SEE_DETAILS,
|
||||
AccompanyingPeriodVoter::CREATE,
|
||||
AccompanyingPeriodVoter::EDIT,
|
||||
AccompanyingPeriodVoter::DELETE
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -43,23 +43,26 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayNode('validation')
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
->booleanNode('center_required')
|
||||
->info('Enable a center for each person entity. If disabled, you must provide your own center provider')
|
||||
->defaultValue(true)
|
||||
->end()
|
||||
->scalarNode('birthdate_not_after')
|
||||
->info($this->validationBirthdateNotAfterInfos)
|
||||
->defaultValue('P1D')
|
||||
->validate()
|
||||
->ifTrue(function($period) {
|
||||
try {
|
||||
$interval = new \DateInterval($period);
|
||||
} catch (\Exception $ex) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Invalid period for birthdate validation : "%s" '
|
||||
. 'The parameter should match duration as defined by ISO8601 : '
|
||||
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
|
||||
->info($this->validationBirthdateNotAfterInfos)
|
||||
->defaultValue('P1D')
|
||||
->validate()
|
||||
->ifTrue(function($period) {
|
||||
try {
|
||||
$interval = new \DateInterval($period);
|
||||
} catch (\Exception $ex) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Invalid period for birthdate validation : "%s" '
|
||||
. 'The parameter should match duration as defined by ISO8601 : '
|
||||
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
|
||||
->end() // birthdate_not_after, parent = children of validation
|
||||
|
||||
->end() // children for 'validation', parent = validation
|
||||
->end() //validation, parent = children of root
|
||||
->end() // children of root, parent = root
|
||||
|
@@ -24,6 +24,8 @@ namespace Chill\PersonBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Entity\HasCentersInterface;
|
||||
use Chill\MainBundle\Entity\HasScopesInterface;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
||||
@@ -52,7 +54,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* "accompanying_period"=AccompanyingPeriod::class
|
||||
* })
|
||||
*/
|
||||
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
|
||||
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface,
|
||||
HasScopesInterface, HasCentersInterface
|
||||
{
|
||||
/**
|
||||
* Mark an accompanying period as "occasional"
|
||||
@@ -809,14 +812,21 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable|Collection
|
||||
*/
|
||||
public function getScopes(): Collection
|
||||
{
|
||||
return $this->scopes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function addScope(Scope $scope): self
|
||||
{
|
||||
$this->scopes[] = $scope;
|
||||
if (!$this->scopes->contains($scope)) {
|
||||
$this->scopes[] = $scope;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -1040,4 +1050,16 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
public function getCenters(): ?iterable
|
||||
{
|
||||
foreach ($this->getPersons() as $person) {
|
||||
if (!in_array($person->getCenter(), $centers ?? [])
|
||||
&& NULL !== $person->getCenter()) {
|
||||
$centers[] = $person->getCenter();
|
||||
}
|
||||
}
|
||||
|
||||
return $centers ?? null;
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,29 @@ use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* This class links a person to the history of his addresses, through
|
||||
* household membership.
|
||||
*
|
||||
* It is optimized on DB side, and compute the start date and end date
|
||||
* of each address by the belonging of household.
|
||||
*
|
||||
* **note**: the start date and end date are the date of belonging to the address,
|
||||
* not the belonging of the household.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* * person A is member of household W from 2021-01-01 to 2021-12-01
|
||||
* * person A is member of household V from 2021-12-01, still present after
|
||||
* * household W lives in address Q from 2020-06-01 to 2021-06-01
|
||||
* * household W lives in address R from 2021-06-01 to 2022-06-01
|
||||
* * household V lives in address T from 2021-12-01 to still living there after
|
||||
*
|
||||
* The person A will have those 3 entities:
|
||||
*
|
||||
* 1. 1st entity: from 2021-01-01 to 2021-06-01, household W, address Q;
|
||||
* 2. 2st entity: from 2021-06-01 to 2021-12-01, household W, address R;
|
||||
* 3. 3st entity: from 2021-12-01 to NULL, household V, address T;
|
||||
*
|
||||
* @ORM\Entity(readOnly=true)
|
||||
* @ORM\Table(name="view_chill_person_household_address")
|
||||
*/
|
||||
@@ -45,11 +68,23 @@ class PersonHouseholdAddress
|
||||
*/
|
||||
private $address;
|
||||
|
||||
/**
|
||||
* The start date of the intersection address/household
|
||||
*
|
||||
* (this is not the startdate of the household, not
|
||||
* the startdate of the address)
|
||||
*/
|
||||
public function getValidFrom(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* The end date of the intersection address/household
|
||||
*
|
||||
* (this is not the enddate of the household, not
|
||||
* the enddate of the address)
|
||||
*/
|
||||
public function getValidTo(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->validTo;
|
||||
|
@@ -35,6 +35,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -43,6 +44,11 @@ use Doctrine\Common\Collections\Criteria;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
|
||||
|
||||
/**
|
||||
* Person Class
|
||||
@@ -57,6 +63,12 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
||||
* @DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "person"=Person::class
|
||||
* })
|
||||
* @PersonHasCenter(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @HouseholdMembershipSequential(
|
||||
* groups={"household_memberships"}
|
||||
* )
|
||||
*/
|
||||
class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
@@ -75,6 +87,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=255)
|
||||
* @Assert\NotBlank(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @Assert\Length(
|
||||
* max=255,
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $firstName;
|
||||
|
||||
@@ -83,6 +102,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=255)
|
||||
* @Assert\NotBlank(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @Assert\Length(
|
||||
* max=255,
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $lastName;
|
||||
|
||||
@@ -102,6 +128,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="date", nullable=true)
|
||||
* @Assert\Date(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @Birthdate(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $birthdate;
|
||||
|
||||
@@ -110,6 +142,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var \DateTimeImmutable
|
||||
*
|
||||
* @ORM\Column(type="date_immutable", nullable=true)
|
||||
* @Assert\Date(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private ?\DateTimeImmutable $deathdate;
|
||||
|
||||
@@ -150,6 +185,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=9, nullable=true)
|
||||
* @Assert\NotNull(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $gender;
|
||||
|
||||
@@ -179,8 +217,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="date", nullable=true)
|
||||
* @Assert\Date(
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $maritalStatusDate;
|
||||
private ?\DateTime $maritalStatusDate;
|
||||
|
||||
/**
|
||||
* Comment on marital status
|
||||
@@ -202,6 +243,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
* @Assert\Email(
|
||||
* checkMX=true,
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $email = '';
|
||||
|
||||
@@ -210,6 +255,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", length=40, nullable=true)
|
||||
* @Assert\Regex(
|
||||
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @PhonenumberConstraint(
|
||||
* type="landline",
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $phonenumber = '';
|
||||
|
||||
@@ -218,6 +271,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", length=40, nullable=true)
|
||||
* @Assert\Regex(
|
||||
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
* @PhonenumberConstraint(
|
||||
* type="mobile",
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $mobilenumber = '';
|
||||
|
||||
@@ -230,12 +291,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* cascade={"persist", "remove", "merge", "detach"},
|
||||
* orphanRemoval=true
|
||||
* )
|
||||
* @Assert\Valid(
|
||||
* traverse=true,
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $otherPhoneNumbers;
|
||||
|
||||
//TO-ADD caseOpeningDate
|
||||
//TO-ADD nativeLanguag
|
||||
|
||||
/**
|
||||
* The person's spoken languages
|
||||
* @var ArrayCollection
|
||||
@@ -254,7 +316,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* @var Center
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $center;
|
||||
|
||||
@@ -353,6 +414,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
private $addresses;
|
||||
|
||||
/**
|
||||
* The current person address.
|
||||
*
|
||||
* This is computed through database and is optimized on database side.
|
||||
*
|
||||
* @var PersonCurrentAddress|null
|
||||
* @ORM\OneToOne(targetEntity=PersonCurrentAddress::class, mappedBy="person")
|
||||
*/
|
||||
private ?PersonCurrentAddress $currentPersonAddress = null;
|
||||
|
||||
/**
|
||||
* fullname canonical. Read-only field, which is calculated by
|
||||
* the database.
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
@@ -373,6 +446,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
private array $currentHouseholdAt = [];
|
||||
|
||||
/**
|
||||
* Read-only field, computed by the database
|
||||
*
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity=PersonHouseholdAddress::class,
|
||||
* mappedBy="person"
|
||||
@@ -390,8 +465,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
|
||||
/**
|
||||
* Person constructor.
|
||||
*
|
||||
* @param \DateTime|null $opening
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
@@ -404,6 +477,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
$this->householdAddresses = new ArrayCollection();
|
||||
$this->genderComment = new CommentEmbeddable();
|
||||
$this->maritalStatusComment = new CommentEmbeddable();
|
||||
$this->periodLocatedOn = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,6 +575,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
return $participation->getAccompanyingPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1179,13 +1255,31 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
return $this->addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getCurrentPersonAddress` instead
|
||||
* @param DateTime|null $from
|
||||
* @return false|mixed|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLastAddress(DateTime $from = null)
|
||||
{
|
||||
$from ??= new DateTime('now');
|
||||
return $this->getCurrentPersonAddress($from);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the address associated with the person at the given date
|
||||
*
|
||||
* @param DateTime|null $at
|
||||
* @return Address|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCurrentPersonAddress(?\DateTime $at = null): ?Address
|
||||
{
|
||||
$at ??= new DateTime('now');
|
||||
|
||||
/** @var ArrayIterator $addressesIterator */
|
||||
$addressesIterator = $this->getAddresses()
|
||||
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $from)
|
||||
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
|
||||
->getIterator();
|
||||
|
||||
$addressesIterator->uasort(
|
||||
@@ -1201,6 +1295,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* Validation callback that checks if the accompanying periods are valid
|
||||
*
|
||||
* This method add violation errors.
|
||||
*
|
||||
* @Assert\Callback(
|
||||
* groups={"accompanying_period_consistent"}
|
||||
* )
|
||||
*/
|
||||
public function isAccompanyingPeriodValid(ExecutionContextInterface $context)
|
||||
{
|
||||
@@ -1246,6 +1344,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* two addresses with the same validFrom date)
|
||||
*
|
||||
* This method add violation errors.
|
||||
*
|
||||
* @Assert\Callback(
|
||||
* groups={"addresses_consistent"}
|
||||
* )
|
||||
*/
|
||||
public function isAddressesValid(ExecutionContextInterface $context)
|
||||
{
|
||||
@@ -1425,7 +1527,16 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
|
||||
public function getCurrentHouseholdAddress(?\DateTimeImmutable $at = null): ?Address
|
||||
{
|
||||
$at = $at === null ? new \DateTimeImmutable('today') : $at;
|
||||
if (
|
||||
NULL === $at
|
||||
||
|
||||
$at->format('Ymd') === (new \DateTime('today'))->format('Ymd')
|
||||
) {
|
||||
return $this->currentPersonAddress instanceof PersonCurrentAddress
|
||||
? $this->currentPersonAddress->getAddress() : NULL;
|
||||
}
|
||||
|
||||
// if not now, compute the date from history
|
||||
$criteria = new Criteria();
|
||||
$expr = Criteria::expr();
|
||||
|
||||
|
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Entity\Person;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Entity which associate person with his current address, through
|
||||
* household participation.
|
||||
*
|
||||
* The computation is optimized on database side.
|
||||
*
|
||||
* The validFrom and validTo properties are the intersection of
|
||||
* household membership and address validity. See @link{PersonHouseholdAddress}
|
||||
*
|
||||
* @ORM\Entity(readOnly=true)
|
||||
* @ORM\Table("view_chill_person_current_address")
|
||||
*/
|
||||
class PersonCurrentAddress
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\OneToOne(targetEntity=Person::class, inversedBy="currentPersonAddress")
|
||||
* @ORM\JoinColumn(name="person_id", referencedColumnName="id")
|
||||
*/
|
||||
protected Person $person;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity=Address::class)
|
||||
*/
|
||||
protected Address $address;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="valid_from", type="date_immutable")
|
||||
*/
|
||||
protected \DateTimeImmutable $validFrom;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="valid_to", type="date_immutable")
|
||||
*/
|
||||
protected ?\DateTimeImmutable $validTo;
|
||||
|
||||
/**
|
||||
* @return Person
|
||||
*/
|
||||
public function getPerson(): Person
|
||||
{
|
||||
return $this->person;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Address
|
||||
*/
|
||||
public function getAddress(): Address
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* This date is the intersection of household membership
|
||||
* and address validity
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getValidFrom(): \DateTimeImmutable
|
||||
{
|
||||
return $this->validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* This date is the intersection of household membership
|
||||
* and address validity
|
||||
*
|
||||
* @return \DateTimeImmutable|null
|
||||
*/
|
||||
public function getValidTo(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->validTo;
|
||||
}
|
||||
}
|
@@ -21,7 +21,9 @@
|
||||
|
||||
namespace Chill\PersonBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -33,6 +35,7 @@ use Chill\PersonBundle\Form\Type\GenderType;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Form\Type\PersonAltNameType;
|
||||
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
||||
|
||||
final class CreationPersonType extends AbstractType
|
||||
{
|
||||
@@ -40,10 +43,6 @@ final class CreationPersonType extends AbstractType
|
||||
// TODO: See if this is still valid and update accordingly.
|
||||
const NAME = 'chill_personbundle_person_creation';
|
||||
|
||||
const FORM_NOT_REVIEWED = 'not_reviewed';
|
||||
const FORM_REVIEWED = 'reviewed' ;
|
||||
const FORM_BEING_REVIEWED = 'being_reviewed';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var CenterTransformer
|
||||
@@ -56,12 +55,16 @@ final class CreationPersonType extends AbstractType
|
||||
*/
|
||||
protected $configPersonAltNamesHelper;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
CenterTransformer $centerTransformer,
|
||||
ConfigPersonAltNamesHelper $configPersonAltNamesHelper
|
||||
ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
|
||||
EventDispatcherInterface $dispatcher
|
||||
) {
|
||||
$this->centerTransformer = $centerTransformer;
|
||||
$this->configPersonAltNamesHelper = $configPersonAltNamesHelper;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +82,9 @@ final class CreationPersonType extends AbstractType
|
||||
->add('gender', GenderType::class, array(
|
||||
'required' => true, 'placeholder' => null
|
||||
))
|
||||
->add('center', CenterType::class)
|
||||
->add('center', CenterType::class, [
|
||||
'required' => false
|
||||
])
|
||||
;
|
||||
|
||||
if ($this->configPersonAltNamesHelper->hasAltNames()) {
|
||||
@@ -87,6 +92,11 @@ final class CreationPersonType extends AbstractType
|
||||
'by_reference' => false
|
||||
]);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
new CustomizeFormEvent(static::class, $builder),
|
||||
CustomizeFormEvent::NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,6 +20,9 @@
|
||||
|
||||
namespace Chill\PersonBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\MaritalStatus;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer;
|
||||
@@ -35,15 +38,13 @@ use Chill\MainBundle\Form\Type\Select2ChoiceType;
|
||||
*/
|
||||
class Select2MaritalStatusType extends AbstractType
|
||||
{
|
||||
/** @var RequestStack */
|
||||
private $requestStack;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
/** @var ObjectManager */
|
||||
private $em;
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
|
||||
public function __construct(RequestStack $requestStack,ObjectManager $em)
|
||||
public function __construct(TranslatableStringHelper $translatableStringHelper, EntityManagerInterface $em)
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
@@ -63,18 +64,17 @@ class Select2MaritalStatusType extends AbstractType
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$locale = $this->requestStack->getCurrentRequest()->getLocale();
|
||||
$maritalStatuses = $this->em->getRepository('Chill\PersonBundle\Entity\MaritalStatus')->findAll();
|
||||
$choices = array();
|
||||
|
||||
foreach ($maritalStatuses as $ms) {
|
||||
$choices[$ms->getId()] = $ms->getName()[$locale];
|
||||
$choices[$ms->getId()] = $this->translatableStringHelper->localize($ms->getName());
|
||||
}
|
||||
|
||||
asort($choices, SORT_STRING | SORT_FLAG_CASE);
|
||||
|
||||
$resolver->setDefaults(array(
|
||||
'class' => 'Chill\PersonBundle\Entity\MaritalStatus',
|
||||
'class' => MaritalStatus::class,
|
||||
'choices' => array_combine(array_values($choices),array_keys($choices))
|
||||
));
|
||||
}
|
||||
|
@@ -18,14 +18,17 @@
|
||||
namespace Chill\PersonBundle\Menu;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Add menu entrie to person menu.
|
||||
*
|
||||
*
|
||||
* Menu entries added :
|
||||
*
|
||||
*
|
||||
* - person details ;
|
||||
* - accompanying period (if `visible`)
|
||||
*
|
||||
@@ -37,21 +40,25 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
* @var string 'visible' or 'hidden'
|
||||
*/
|
||||
protected $showAccompanyingPeriod;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(
|
||||
$showAccompanyingPeriod,
|
||||
ParameterBagInterface $parameterBag,
|
||||
Security $security,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->showAccompanyingPeriod = $showAccompanyingPeriod;
|
||||
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
|
||||
$this->security = $security;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
$menu->addChild($this->translator->trans('Person details'), [
|
||||
@@ -83,8 +90,10 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
->setExtras([
|
||||
'order' => 99999
|
||||
]);
|
||||
|
||||
if ($this->showAccompanyingPeriod === 'visible') {
|
||||
|
||||
if ($this->showAccompanyingPeriod === 'visible'
|
||||
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])
|
||||
) {
|
||||
$menu->addChild($this->translator->trans('Accompanying period list'), [
|
||||
'route' => 'chill_person_accompanying_period_list',
|
||||
'routeParameters' => [
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
|
||||
{
|
||||
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||
private Security $security;
|
||||
private AuthorizationHelper $authorizationHelper;
|
||||
private CenterResolverDispatcher $centerResolverDispatcher;
|
||||
|
||||
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, AuthorizationHelper $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher)
|
||||
{
|
||||
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
|
||||
$this->security = $security;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
||||
}
|
||||
|
||||
public function findByPerson(
|
||||
Person $person,
|
||||
string $role,
|
||||
?array $orderBy = [],
|
||||
int $limit = null,
|
||||
int $offset = null
|
||||
): array {
|
||||
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
|
||||
$scopes = $this->authorizationHelper
|
||||
->getReachableCircles($this->security->getUser(), $role,
|
||||
$this->centerResolverDispatcher->resolveCenter($person));
|
||||
|
||||
if (0 === count($scopes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb
|
||||
->join('ap.participations', 'participation')
|
||||
->where($qb->expr()->eq('participation.person', ':person'))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
'ap.confidential = FALSE',
|
||||
$qb->expr()->eq('ap.user', ':user')
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->neq('ap.step', ':draft'),
|
||||
$qb->expr()->eq('ap.createdBy', ':creator')
|
||||
)
|
||||
)
|
||||
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT)
|
||||
->setParameter('person', $person)
|
||||
->setParameter('user', $this->security->getUser())
|
||||
->setParameter('creator', $this->security->getUser())
|
||||
;
|
||||
// add join condition for scopes
|
||||
$orx = $qb->expr()->orX(
|
||||
$qb->expr()->eq('ap.step', ':draft')
|
||||
);
|
||||
|
||||
foreach ($scopes as $key => $scope) {
|
||||
$orx->add($qb->expr()->isMemberOf(':scope_'.$key, 'ap.scopes'));
|
||||
$qb->setParameter('scope_'.$key, $scope);
|
||||
}
|
||||
$qb->andWhere($orx);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
|
||||
interface AccompanyingPeriodACLAwareRepositoryInterface
|
||||
{
|
||||
public function findByPerson(
|
||||
Person $person,
|
||||
string $role,
|
||||
?array $orderBy = [],
|
||||
int $limit = null,
|
||||
int $offset = null
|
||||
): array;
|
||||
}
|
@@ -59,6 +59,11 @@ final class AccompanyingPeriodRepository implements ObjectRepository
|
||||
return $this->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
|
||||
{
|
||||
return $this->repository->createQueryBuilder($alias, $indexBy);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return AccompanyingPeriod::class;
|
||||
|
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
|
||||
final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
|
||||
{
|
||||
private Security $security;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private CountryRepository $countryRepository;
|
||||
|
||||
private AuthorizationHelper $authorizationHelper;
|
||||
|
||||
public function __construct(
|
||||
Security $security,
|
||||
EntityManagerInterface $em,
|
||||
CountryRepository $countryRepository,
|
||||
AuthorizationHelper $authorizationHelper
|
||||
) {
|
||||
$this->security = $security;
|
||||
$this->em = $em;
|
||||
$this->countryRepository = $countryRepository;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Person[]
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public function findBySearchCriteria(
|
||||
int $start,
|
||||
int $limit,
|
||||
bool $simplify = false,
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): array {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): int {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
|
||||
return $this->getCountQueryResult($qb,'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function getCountQueryResult(QueryBuilder $qb, $alias): int
|
||||
{
|
||||
$qb->select('COUNT('.$alias.'.id)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function findBySimilaritySearch(string $pattern, int $firstResult,
|
||||
int $maxResult, bool $simplify = false)
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
|
||||
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
|
||||
}
|
||||
|
||||
public function countBySimilaritySearch(string $pattern)
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
|
||||
return $this->getCountQueryResult($qb, 'sp');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function createSearchQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): QueryBuilder {
|
||||
|
||||
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');
|
||||
|
||||
if (NULL !== $firstname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
|
||||
->setParameter('firstname', '%'.$firstname.'%');
|
||||
}
|
||||
|
||||
if (NULL !== $lastname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
|
||||
->setParameter('lastname', '%'.$lastname.'%');
|
||||
}
|
||||
|
||||
if (NULL !== $birthdate) {
|
||||
$qb->andWhere($qb->expr()->eq('s.birthdate', ':birthdate'))
|
||||
->setParameter('birthdate', $birthdate);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
|
||||
->setParameter('birthdateafter', $birthdateAfter);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateBefore) {
|
||||
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
|
||||
->setParameter('birthdatebefore', $birthdateBefore);
|
||||
}
|
||||
|
||||
if (NULL !== $gender) {
|
||||
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
|
||||
->setParameter('gender', $gender);
|
||||
}
|
||||
|
||||
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 !== $default) {
|
||||
$grams = explode(' ', $default);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere($qb->expr()
|
||||
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
}
|
||||
}
|
||||
|
||||
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.'%');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
|
||||
interface PersonACLAwareRepositoryInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @return array|Person[]
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public function findBySearchCriteria(
|
||||
int $start,
|
||||
int $limit,
|
||||
bool $simplify = false,
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): array;
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
);
|
||||
|
||||
public function findBySimilaritySearch(
|
||||
string $pattern,
|
||||
int $firstResult,
|
||||
int $maxResult,
|
||||
bool $simplify = false
|
||||
);
|
||||
|
||||
public function countBySimilaritySearch(string $pattern);
|
||||
}
|
@@ -23,9 +23,11 @@ use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use UnexpectedValueException;
|
||||
|
||||
|
||||
final class PersonRepository
|
||||
final class PersonRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
@@ -44,6 +46,26 @@ final class PersonRepository
|
||||
return $this->repository->findBy(['id' => $ids]);
|
||||
}
|
||||
|
||||
public function findAll()
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria)
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Person::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $centers
|
||||
* @param $firstResult
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<origin-demand></origin-demand>
|
||||
<requestor></requestor>
|
||||
<social-issue></social-issue>
|
||||
<scopes></scopes>
|
||||
<referrer></referrer>
|
||||
<resources></resources>
|
||||
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
|
||||
@@ -32,6 +33,7 @@ import PersonsAssociated from './components/PersonsAssociated.vue';
|
||||
import Requestor from './components/Requestor.vue';
|
||||
import SocialIssue from './components/SocialIssue.vue';
|
||||
import CourseLocation from './components/CourseLocation.vue';
|
||||
import Scopes from './components/Scopes.vue';
|
||||
import Referrer from './components/Referrer.vue';
|
||||
import Resources from './components/Resources.vue';
|
||||
import Comment from './components/Comment.vue';
|
||||
@@ -47,6 +49,7 @@ export default {
|
||||
Requestor,
|
||||
SocialIssue,
|
||||
CourseLocation,
|
||||
Scopes,
|
||||
Referrer,
|
||||
Resources,
|
||||
Comment,
|
||||
|
@@ -191,7 +191,49 @@ const getListOrigins = () => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw { msg: 'Error while retriving origin\'s list.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addScope = (id, scope) => {
|
||||
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
|
||||
console.log(url);
|
||||
console.log(scope);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: scope.id,
|
||||
type: scope.type,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
|
||||
});
|
||||
};
|
||||
|
||||
const removeScope = (id, scope) => {
|
||||
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
|
||||
console.log(url);
|
||||
console.log(scope);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
id: scope.id,
|
||||
type: scope.type,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
getAccompanyingCourse,
|
||||
@@ -204,5 +246,7 @@ export {
|
||||
getUsers,
|
||||
whoami,
|
||||
getListOrigins,
|
||||
postSocialIssue
|
||||
postSocialIssue,
|
||||
addScope,
|
||||
removeScope,
|
||||
};
|
||||
|
@@ -88,6 +88,10 @@ export default {
|
||||
socialIssue: {
|
||||
msg: 'confirm.socialIssue_not_valid',
|
||||
anchor: '#section-50'
|
||||
},
|
||||
scopes: {
|
||||
msg: 'confirm.set_a_scope',
|
||||
anchor: '#section-65'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h2><a name="section-65"></a>{{ $t('scopes.title') }}</h2>
|
||||
|
||||
<ul>
|
||||
<li v-for="s in scopes">
|
||||
<input type="checkbox" v-model="checkedScopes" :value="s" />
|
||||
{{ s.name.fr }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="!isScopeValid" class="alert alert-warning separator">
|
||||
{{ $t('scopes.add_at_least_one') }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "Scopes",
|
||||
computed: {
|
||||
...mapState([
|
||||
'scopes',
|
||||
'scopesAtStart'
|
||||
]),
|
||||
...mapGetters([
|
||||
'isScopeValid'
|
||||
]),
|
||||
checkedScopes: {
|
||||
get: function() {
|
||||
return this.$store.state.accompanyingCourse.scopes;
|
||||
},
|
||||
set: function(v) {
|
||||
this.$store.dispatch('setScopes', v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -86,6 +86,10 @@ const appMessages = {
|
||||
person_locator: "Parcours localisé auprès de {0}",
|
||||
no_address: "Il n'y a pas d'adresse associée au parcours"
|
||||
},
|
||||
scopes: {
|
||||
title: "Services",
|
||||
add_at_least_one: "Indiquez au moins un service",
|
||||
},
|
||||
referrer: {
|
||||
title: "Référent du parcours",
|
||||
label: "Vous pouvez choisir un TMS ou vous assigner directement comme référent",
|
||||
@@ -113,6 +117,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",
|
||||
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 !",
|
||||
ok: "Confirmer le parcours"
|
||||
|
@@ -1,28 +1,41 @@
|
||||
import 'es6-promise/auto';
|
||||
import { createStore } from 'vuex';
|
||||
import { fetchScopes } from 'ChillMainAssets/lib/api/scope.js';
|
||||
import { getAccompanyingCourse,
|
||||
patchAccompanyingCourse,
|
||||
confirmAccompanyingCourse,
|
||||
postParticipation,
|
||||
postRequestor,
|
||||
postResource,
|
||||
postSocialIssue } from '../api';
|
||||
postSocialIssue,
|
||||
addScope,
|
||||
removeScope,
|
||||
} from '../api';
|
||||
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
const id = window.accompanyingCourseId;
|
||||
|
||||
let initPromise = getAccompanyingCourse(id)
|
||||
.then(accompanying_course => new Promise((resolve, reject) => {
|
||||
let scopesPromise = fetchScopes();
|
||||
let accompanyingCoursePromise = getAccompanyingCourse(id);
|
||||
|
||||
let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
|
||||
.then(([scopes, accompanyingCourse]) => new Promise((resolve, reject) => {
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
modules: {
|
||||
},
|
||||
state: {
|
||||
accompanyingCourse: accompanying_course,
|
||||
accompanyingCourse: accompanyingCourse,
|
||||
addressContext: {},
|
||||
errorMsg: []
|
||||
errorMsg: [],
|
||||
// all the available scopes
|
||||
scopes: scopes,
|
||||
// the scopes at start. If the user remove all scopes, we re-add those scopes, by security
|
||||
scopesAtStart: accompanyingCourse.scopes.map(scope => scope),
|
||||
// the scope states at server side
|
||||
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
|
||||
},
|
||||
getters: {
|
||||
isParticipationValid(state) {
|
||||
@@ -34,11 +47,16 @@ let initPromise = getAccompanyingCourse(id)
|
||||
isLocationValid(state) {
|
||||
return state.accompanyingCourse.location !== null;
|
||||
},
|
||||
isScopeValid(state) {
|
||||
console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
|
||||
return state.accompanyingCourse.scopes.length > 0;
|
||||
},
|
||||
validationKeys(state, getters) {
|
||||
let keys = [];
|
||||
if (!getters.isParticipationValid) { keys.push('participation'); }
|
||||
if (!getters.isLocationValid) { keys.push('location'); }
|
||||
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
|
||||
if (!getters.isScopeValid) { keys.push('scopes'); }
|
||||
//console.log('getter keys', keys);
|
||||
return keys;
|
||||
},
|
||||
@@ -142,6 +160,21 @@ let initPromise = getAccompanyingCourse(id)
|
||||
//console.log('### mutation: set edit context true with addressId', payload.addressId);
|
||||
state.addressContext.edit = true;
|
||||
state.addressContext.addressId = payload.addressId;
|
||||
},
|
||||
setScopes(state, scopes) {
|
||||
state.accompanyingCourse.scopes = scopes;
|
||||
},
|
||||
addScopeAtBackend(state, scope) {
|
||||
let scopeIds = state.scopesAtBackend.map(s => s.id);
|
||||
if (!scopeIds.includes(scope.id)) {
|
||||
state.scopesAtBackend.push(scope);
|
||||
}
|
||||
},
|
||||
removeScopeAtBackend(state, scope){
|
||||
let scopeIds = state.scopesAtBackend.map(s => s.id);
|
||||
if (scopeIds.includes(scope.id)) {
|
||||
state.scopesAtBackend = state.scopesAtBackend.filter(s => s.id !== scope.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -228,6 +261,107 @@ let initPromise = getAccompanyingCourse(id)
|
||||
resolve();
|
||||
})).catch((error) => { commit('catchError', error) });
|
||||
},
|
||||
/**
|
||||
* Handle the checked/unchecked scopes
|
||||
*
|
||||
* When the user set the scopes in a invalid situation (when no scopes are cheched), this
|
||||
* method will internally re-add the scopes as they were originally when the page was loaded, but
|
||||
* this does not appears for the user (they remains unchecked). When the user re-add a scope, the
|
||||
* scope is back in a valid state, and the store synchronize with the new state (all the original scopes
|
||||
* are removed if necessary, and the new checked scopes is backed).
|
||||
*
|
||||
* So, for instance:
|
||||
*
|
||||
* at load:
|
||||
*
|
||||
* [x] scope A (at backend: [x])
|
||||
* [x] scope B (at backend: [x])
|
||||
* [ ] scope C (at backend: [ ])
|
||||
*
|
||||
* The user uncheck scope A:
|
||||
*
|
||||
* [ ] scope A (at backend: [ ] as soon as the operation finish)
|
||||
* [x] scope B (at backend: [x])
|
||||
* [ ] scope C (at backend: [ ])
|
||||
*
|
||||
* The user uncheck scope B. The state is invalid (no scope checked), so we go back to initial state when
|
||||
* the page loaded):
|
||||
*
|
||||
* [ ] scope A (at backend: [x] as soon as the operation finish)
|
||||
* [ ] scope B (at backend: [x] as soon as the operation finish)
|
||||
* [ ] scope C (at backend: [ ])
|
||||
*
|
||||
* The user check scope C. The scopes are back to valid state. So we go back to synchronization with UI and
|
||||
* backend):
|
||||
*
|
||||
* [ ] scope A (at backend: [ ] as soon as the operation finish)
|
||||
* [ ] scope B (at backend: [ ] as soon as the operation finish)
|
||||
* [x] scope C (at backend: [x] as soon as the operation finish)
|
||||
*
|
||||
* **Warning** There is a problem if the user check/uncheck faster than the backend is synchronized.
|
||||
*
|
||||
* @param commit
|
||||
* @param state
|
||||
* @param dispatch
|
||||
* @param scopes
|
||||
* @returns Promise
|
||||
*/
|
||||
setScopes({ commit, state, dispatch }, scopes) {
|
||||
let currentServerScopesIds = state.scopesAtBackend.map(scope => scope.id);
|
||||
let checkedScopesIds = scopes.map(scope => scope.id);
|
||||
let removedScopesIds = currentServerScopesIds.filter(id => !checkedScopesIds.includes(id));
|
||||
let addedScopesIds = checkedScopesIds.filter(id => !currentServerScopesIds.includes(id));
|
||||
let lengthAfterOperation = currentServerScopesIds.length + addedScopesIds.length
|
||||
- removedScopesIds.length;
|
||||
|
||||
if (lengthAfterOperation > 0 || (lengthAfterOperation === 0 && state.scopesAtStart.length === 0) ) {
|
||||
return dispatch('updateScopes', {
|
||||
addedScopesIds, removedScopesIds
|
||||
}).then(() => {
|
||||
// warning: when the operation of dispatch are too slow, the user may check / uncheck before
|
||||
// the end of the synchronisation with the server (done by dispatch operation). Then, it leads to
|
||||
// check/uncheck in the UI. I do not know of to avoid it.
|
||||
commit('setScopes', scopes);
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else {
|
||||
return dispatch('setScopes', state.scopesAtStart).then(() => {
|
||||
commit('setScopes', scopes);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Internal function for the store to effectively update scopes.
|
||||
*
|
||||
* Return a promise which resolves when all update operation are
|
||||
* successful and finished.
|
||||
*
|
||||
* @param state
|
||||
* @param commit
|
||||
* @param addedScopesIds
|
||||
* @param removedScopesIds
|
||||
* @return Promise
|
||||
*/
|
||||
updateScopes({ state, commit }, { addedScopesIds, removedScopesIds }) {
|
||||
let promises = [];
|
||||
state.scopes.forEach(scope => {
|
||||
if (addedScopesIds.includes(scope.id)) {
|
||||
promises.push(addScope(state.accompanyingCourse.id, scope).then(() => {
|
||||
commit('addScopeAtBackend', scope);
|
||||
return Promise.resolve();
|
||||
}));
|
||||
}
|
||||
if (removedScopesIds.includes(scope.id)) {
|
||||
promises.push(removeScope(state.accompanyingCourse.id, scope).then(() => {
|
||||
commit('removeScopeAtBackend', scope);
|
||||
return Promise.resolve();
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
postFirstComment({ commit }, payload) {
|
||||
//console.log('## action: postFirstComment: payload', payload);
|
||||
patchAccompanyingCourse(id, { type: "accompanying_period", initialComment: payload })
|
||||
|
@@ -146,10 +146,10 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if options['addCenter'] %}
|
||||
{% if options['addCenter'] and person|chill_resolve_center is not null %}
|
||||
<li>
|
||||
<i class="fa fa-li fa-long-arrow-right"></i>
|
||||
{{ person.center }}
|
||||
{{ person|chill_resolve_center.name }}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@@ -17,11 +17,11 @@
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="text-md-end">
|
||||
{%- if chill_person.fields.spoken_languages == 'visible' -%}
|
||||
{% if person|chill_resolve_center is not null%}
|
||||
<span class="open_sansbold">
|
||||
{{ 'Center'|trans|upper}} :
|
||||
</span>
|
||||
{{ person.center.name|upper }}
|
||||
{{ person|chill_resolve_center.name|upper }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -76,10 +76,9 @@
|
||||
|
||||
{{ form_row(form.gender, { 'label' : 'Gender'|trans }) }}
|
||||
|
||||
<div style="display: none">
|
||||
{# TODO remove this field (vendee) #}
|
||||
{{ form_row(form.center, { 'label' : 'Center'|trans }) }}
|
||||
</div>
|
||||
{% if form.center is defined %}
|
||||
{{ form_row(form.center) }}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="dropdown">
|
||||
|
@@ -20,71 +20,39 @@
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
||||
HasAdvancedSearchFormInterface
|
||||
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var \Chill\MainBundle\Entity\User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginatorFactory;
|
||||
protected EngineInterface $templating;
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
|
||||
const NAME = "person_regular";
|
||||
|
||||
private const POSSIBLE_KEYS = [
|
||||
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
|
||||
'birthdate-after', 'gender', 'nationality'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $helper,
|
||||
PaginatorFactory $paginatorFactory)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->user = $tokenStorage->getToken()->getUser();
|
||||
$this->helper = $helper;
|
||||
EngineInterface $templating,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository
|
||||
) {
|
||||
$this->templating = $templating;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
|
||||
// throw an error if user is not a valid user
|
||||
if (!$this->user instanceof \Chill\MainBundle\Entity\User) {
|
||||
throw new \LogicException('The user provided must be an instance'
|
||||
. ' of Chill\MainBundle\Entity\User');
|
||||
}
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -120,12 +88,14 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
if ($format === 'html') {
|
||||
return $this->container->get('templating')->render('@ChillPerson/Person/list_with_period.html.twig',
|
||||
return $this->templating->render('@ChillPerson/Person/list_with_period.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']),
|
||||
'pattern' => $this->recomposePattern(
|
||||
$terms,
|
||||
\array_filter(self::POSSIBLE_KEYS, fn($item) => $item !== '_default'),
|
||||
$terms['_domain']
|
||||
),
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
@@ -152,153 +122,81 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
||||
*/
|
||||
protected function search(array $terms, $start, $limit, array $options = array())
|
||||
{
|
||||
$qb = $this->createQuery($terms, 'search');
|
||||
|
||||
if ($options['simplify'] ?? false) {
|
||||
$qb->select(
|
||||
'p.id',
|
||||
$qb->expr()->concat(
|
||||
'p.firstName',
|
||||
$qb->expr()->literal(' '),
|
||||
'p.lastName'
|
||||
).'AS text'
|
||||
);
|
||||
} else {
|
||||
$qb->select('p');
|
||||
[
|
||||
'_default' => $default,
|
||||
'firstname' => $firstname,
|
||||
'lastname' => $lastname,
|
||||
'birthdate' => $birthdate,
|
||||
'birthdate-before' => $birthdateBefore,
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
if (NULL !== ${$v}) {
|
||||
try {
|
||||
${$v} = new \DateTime(${$v});
|
||||
} catch (\Exception $e) {
|
||||
throw new ParsingException('The date is '
|
||||
. 'not parsable', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$qb
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($start);
|
||||
|
||||
//order by firstname, lastname
|
||||
|
||||
$qb
|
||||
->orderBy('p.firstName')
|
||||
->addOrderBy('p.lastName');
|
||||
|
||||
if ($options['simplify'] ?? false) {
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
||||
} else {
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
return $this->personACLAwareRepository
|
||||
->findBySearchCriteria(
|
||||
$start,
|
||||
$limit,
|
||||
$options['simplify'] ?? false,
|
||||
$default,
|
||||
$firstname,
|
||||
$lastname,
|
||||
$birthdate,
|
||||
$birthdateBefore,
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
);
|
||||
}
|
||||
|
||||
protected function count(array $terms)
|
||||
protected function count(array $terms): int
|
||||
{
|
||||
$qb = $this->createQuery($terms);
|
||||
[
|
||||
'_default' => $default,
|
||||
'firstname' => $firstname,
|
||||
'lastname' => $lastname,
|
||||
'birthdate' => $birthdate,
|
||||
'birthdate-before' => $birthdateBefore,
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
|
||||
$qb->select('COUNT(p.id)');
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
|
||||
private $_cacheQuery = array();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $terms
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
*/
|
||||
public function createQuery(array $terms)
|
||||
{
|
||||
//get from cache
|
||||
$cacheKey = md5(serialize($terms));
|
||||
if (array_key_exists($cacheKey, $this->_cacheQuery)) {
|
||||
return clone $this->_cacheQuery[$cacheKey];
|
||||
}
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb->from('ChillPersonBundle:Person', 'p');
|
||||
|
||||
if (array_key_exists('firstname', $terms)) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
|
||||
->setParameter('firstname', '%'.$terms['firstname'].'%');
|
||||
}
|
||||
|
||||
if (array_key_exists('lastname', $terms)) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
|
||||
->setParameter('lastname', '%'.$terms['lastname'].'%');
|
||||
}
|
||||
|
||||
foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key)
|
||||
if (array_key_exists($key, $terms)) {
|
||||
try {
|
||||
$date = new \DateTime($terms[$key]);
|
||||
} catch (\Exception $ex) {
|
||||
throw new ParsingException('The date is '
|
||||
. 'not parsable', 0, $ex);
|
||||
}
|
||||
|
||||
switch($key) {
|
||||
case 'birthdate':
|
||||
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
|
||||
->setParameter('birthdate', $date);
|
||||
break;
|
||||
case 'birthdate-before':
|
||||
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
|
||||
->setParameter('birthdatebefore', $date);
|
||||
break;
|
||||
case 'birthdate-after':
|
||||
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
|
||||
->setParameter('birthdateafter', $date);
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException("this case $key should not exists");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (array_key_exists('gender', $terms)) {
|
||||
if (!in_array($terms['gender'], array(Person::MALE_GENDER, Person::FEMALE_GENDER))) {
|
||||
throw new ParsingException('The gender '
|
||||
.$terms['gender'].' is not accepted. Should be "'.Person::MALE_GENDER
|
||||
.'" or "'.Person::FEMALE_GENDER.'"');
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
|
||||
->setParameter('gender', $terms['gender']);
|
||||
}
|
||||
|
||||
if (array_key_exists('nationality', $terms)) {
|
||||
try {
|
||||
$country = $this->em->createQuery('SELECT c FROM '
|
||||
. 'ChillMainBundle:Country c WHERE '
|
||||
. 'LOWER(c.countryCode) LIKE :code')
|
||||
->setParameter('code', $terms['nationality'])
|
||||
->getSingleResult();
|
||||
} catch (\Doctrine\ORM\NoResultException $ex) {
|
||||
throw new ParsingException('The country code "'.$terms['nationality'].'" '
|
||||
. ', used in nationality, is unknow', 0, $ex);
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
|
||||
->setParameter('nationality', $country);
|
||||
}
|
||||
|
||||
if ($terms['_default'] !== '') {
|
||||
$grams = explode(' ', $terms['_default']);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere($qb->expr()
|
||||
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
if (NULL !== ${$v}) {
|
||||
try {
|
||||
${$v} = new \DateTime(${$v});
|
||||
} catch (\Exception $e) {
|
||||
throw new ParsingException('The date is '
|
||||
. 'not parsable', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//restraint center for security
|
||||
$reachableCenters = $this->helper->getReachableCenters($this->user,
|
||||
new Role('CHILL_PERSON_SEE'));
|
||||
$qb->andWhere($qb->expr()
|
||||
->in('p.center', ':centers'))
|
||||
->setParameter('centers', $reachableCenters)
|
||||
;
|
||||
|
||||
$this->_cacheQuery[$cacheKey] = $qb;
|
||||
|
||||
return clone $qb;
|
||||
return $this->personACLAwareRepository
|
||||
->countBySearchCriteria(
|
||||
$default,
|
||||
$firstname,
|
||||
$lastname,
|
||||
$birthdate,
|
||||
$birthdateBefore,
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
@@ -391,4 +289,10 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
||||
return 'Search within persons';
|
||||
}
|
||||
|
||||
public static function getAlias(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -2,90 +2,32 @@
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
* Class SimilarityPersonSearch
|
||||
*
|
||||
* @package Chill\PersonBundle\Search
|
||||
*/
|
||||
class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var \Chill\MainBundle\Entity\User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginatorFactory;
|
||||
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
|
||||
private EngineInterface $templating;
|
||||
|
||||
const NAME = "person_similarity";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PersonSearch
|
||||
*/
|
||||
private $personSearch;
|
||||
|
||||
|
||||
/**
|
||||
* SimilarityPersonSearch constructor.
|
||||
*
|
||||
* @param EntityManagerInterface $em
|
||||
* @param TokenStorageInterface $tokenStorage
|
||||
* @param AuthorizationHelper $helper
|
||||
* @param PaginatorFactory $paginatorFactory
|
||||
* @param PersonSearch $personSearch
|
||||
*/
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $helper,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonSearch $personSearch)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->user = $tokenStorage->getToken()->getUser();
|
||||
$this->helper = $helper;
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
EngineInterface $templating
|
||||
) {
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personSearch = $personSearch;
|
||||
|
||||
// throw an error if user is not a valid user
|
||||
if (!$this->user instanceof \Chill\MainBundle\Entity\User) {
|
||||
throw new \LogicException('The user provided must be an instance'
|
||||
. ' of Chill\MainBundle\Entity\User');
|
||||
}
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->templating = $templating;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
|
||||
@@ -94,7 +36,7 @@ class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
|
||||
@@ -103,30 +45,27 @@ class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
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
|
||||
* @return array
|
||||
*/
|
||||
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->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig',
|
||||
|
||||
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',
|
||||
@@ -143,9 +82,8 @@ class SimilarityPersonSearch extends AbstractSearch
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} elseif ($format === 'json')
|
||||
{
|
||||
|
||||
} elseif ($format === 'json') {
|
||||
return [
|
||||
'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])),
|
||||
'pagination' => [
|
||||
@@ -154,8 +92,7 @@ class SimilarityPersonSearch extends AbstractSearch
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $pattern
|
||||
@@ -166,101 +103,12 @@ class SimilarityPersonSearch extends AbstractSearch
|
||||
*/
|
||||
protected function search(array $terms, $start, $limit, array $options = array())
|
||||
{
|
||||
$qb = $this->createQuery($terms, 'search');
|
||||
|
||||
|
||||
if ($options['simplify'] ?? false) {
|
||||
$qb->select(
|
||||
'sp.id',
|
||||
$qb->expr()->concat(
|
||||
'sp.firstName',
|
||||
$qb->expr()->literal(' '),
|
||||
'sp.lastName'
|
||||
).'AS text'
|
||||
);
|
||||
} else {
|
||||
$qb->select('sp');
|
||||
}
|
||||
|
||||
$qb
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($start);
|
||||
|
||||
//order by firstname, lastname
|
||||
|
||||
$qb
|
||||
->orderBy('sp.firstName')
|
||||
->addOrderBy('sp.lastName');
|
||||
|
||||
if ($options['simplify'] ?? false) {
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
||||
} else {
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
return $this->personACLAwareRepository
|
||||
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function count(array $terms)
|
||||
{
|
||||
$qb = $this->createQuery($terms);
|
||||
|
||||
|
||||
$qb->select('COUNT(sp.id)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
|
||||
}
|
||||
|
||||
|
||||
private $_cacheQuery = array();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $terms
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
*/
|
||||
protected function createQuery(array $terms)
|
||||
{
|
||||
//get from cache
|
||||
$cacheKey = md5(serialize($terms));
|
||||
if (array_key_exists($cacheKey, $this->_cacheQuery)) {
|
||||
return clone $this->_cacheQuery[$cacheKey];
|
||||
}
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb ->select('sp')
|
||||
->from('ChillPersonBundle:Person', 'sp');
|
||||
|
||||
if ($terms['_default'] !== '') {
|
||||
$grams = explode(' ', $terms['_default']);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere('SIMILARITY(sp.fullnameCanonical, UNACCENT(LOWER(:default_'.$key.')) ) >= 0.15')
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()
|
||||
->notIn(
|
||||
'sp.id',
|
||||
$this->personSearch
|
||||
->createQuery($terms)
|
||||
->addSelect('p.id')
|
||||
->getDQL()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
//restraint center for security
|
||||
$reachableCenters = $this->helper->getReachableCenters($this->user,
|
||||
new Role('CHILL_PERSON_SEE'));
|
||||
$qb->andWhere($qb->expr()
|
||||
->in('sp.center', ':centers'))
|
||||
->setParameter('centers', $reachableCenters)
|
||||
;
|
||||
|
||||
$this->_cacheQuery[$cacheKey] = $qb;
|
||||
|
||||
return clone $qb;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -4,73 +4,101 @@ namespace Chill\PersonBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
protected AuthorizationHelper $helper;
|
||||
|
||||
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
|
||||
/**
|
||||
* details are for seeing:
|
||||
*
|
||||
* * SocialIssues
|
||||
*/
|
||||
public const SEE_DETAILS = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS';
|
||||
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
|
||||
public const EDIT = 'CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE';
|
||||
public const DELETE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_DELETE';
|
||||
|
||||
/**
|
||||
* @param AuthorizationHelper $helper
|
||||
* Give all the right above
|
||||
*/
|
||||
public function __construct(AuthorizationHelper $helper)
|
||||
{
|
||||
$this->helper = $helper;
|
||||
public const FULL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_FULL';
|
||||
|
||||
public const ALL = [
|
||||
self::SEE,
|
||||
self::SEE_DETAILS,
|
||||
self::CREATE,
|
||||
self::EDIT,
|
||||
self::DELETE,
|
||||
self::FULL,
|
||||
];
|
||||
|
||||
private VoterHelperInterface $voterHelper;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(
|
||||
Security $security,
|
||||
VoterHelperFactoryInterface $voterHelperFactory
|
||||
) {
|
||||
$this->security = $security;
|
||||
$this->voterHelper = $voterHelperFactory
|
||||
->generate(self::class)
|
||||
->addCheckFor(null, [self::CREATE])
|
||||
->addCheckFor(AccompanyingPeriod::class, self::ALL)
|
||||
->addCheckFor(Person::class, [self::SEE])
|
||||
->build();
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
return $subject instanceof AccompanyingPeriod;
|
||||
return $this->voterHelper->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO take scopes into account
|
||||
if (count($subject->getPersons()) === 0) {
|
||||
return true;
|
||||
}
|
||||
foreach ($subject->getPersons() as $person) {
|
||||
// give access as soon as on center is reachable
|
||||
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {
|
||||
return true;
|
||||
if ($subject instanceof AccompanyingPeriod) {
|
||||
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
|
||||
// only creator can see, edit, delete, etc.
|
||||
if ($subject->getCreatedBy() === $token->getUser()
|
||||
|| NULL === $subject->getCreatedBy()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
// if confidential, only the referent can see it
|
||||
if ($subject->isConfidential()) {
|
||||
return $token->getUser() === $subject->getUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getAttributes()
|
||||
{
|
||||
return [
|
||||
self::SEE
|
||||
];
|
||||
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
|
||||
}
|
||||
|
||||
public function getRoles()
|
||||
{
|
||||
return $this->getAttributes();
|
||||
return self::ALL;
|
||||
}
|
||||
|
||||
public function getRolesWithoutScope()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function getRolesWithHierarchy()
|
||||
{
|
||||
return [ 'Person' => $this->getRoles() ];
|
||||
return [ 'Accompanying period' => $this->getRoles() ];
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -22,17 +22,15 @@ namespace Chill\PersonBundle\Security\Authorization;
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
const CREATE = 'CHILL_PERSON_CREATE';
|
||||
@@ -41,51 +39,31 @@ class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte
|
||||
const STATS = 'CHILL_PERSON_STATS';
|
||||
const LISTS = 'CHILL_PERSON_LISTS';
|
||||
const DUPLICATE = 'CHILL_PERSON_DUPLICATE';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $helper;
|
||||
|
||||
public function __construct(AuthorizationHelper $helper)
|
||||
{
|
||||
$this->helper = $helper;
|
||||
|
||||
protected VoterHelperInterface $voter;
|
||||
|
||||
public function __construct(
|
||||
VoterHelperFactoryInterface $voterFactory
|
||||
) {
|
||||
$this->voter = $voterFactory
|
||||
->generate(self::class)
|
||||
->addCheckFor(Center::class, [self::STATS, self::LISTS, self::DUPLICATE])
|
||||
->addCheckFor(Person::class, [self::CREATE, self::UPDATE, self::SEE, self::DUPLICATE])
|
||||
->addCheckFor(null, [self::CREATE] )
|
||||
->build()
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if ($subject instanceof Person) {
|
||||
return \in_array($attribute, [
|
||||
self::CREATE, self::UPDATE, self::SEE, self::DUPLICATE
|
||||
]);
|
||||
} elseif ($subject instanceof Center) {
|
||||
return \in_array($attribute, [
|
||||
self::STATS, self::LISTS, self::DUPLICATE
|
||||
]);
|
||||
} elseif ($subject === null) {
|
||||
return $attribute === self::CREATE;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return $this->voter->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject === null) {
|
||||
$centers = $this->helper->getReachableCenters($token->getUser(),
|
||||
new Role($attribute));
|
||||
|
||||
return count($centers) > 0;
|
||||
}
|
||||
|
||||
return $this->helper->userHasAccess($token->getUser(), $subject, $attribute);
|
||||
return $this->voter->voteOnAttribute($attribute, $subject, $token);
|
||||
}
|
||||
|
||||
|
||||
private function getAttributes()
|
||||
{
|
||||
return array(self::CREATE, self::UPDATE, self::SEE, self::STATS, self::LISTS, self::DUPLICATE);
|
||||
@@ -100,7 +78,7 @@ class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte
|
||||
{
|
||||
return $this->getAttributes();
|
||||
}
|
||||
|
||||
|
||||
public function getRolesWithHierarchy()
|
||||
{
|
||||
return [ 'Person' => $this->getRoles() ];
|
||||
|
@@ -23,6 +23,7 @@
|
||||
namespace Chill\PersonBundle\Tests\Controller;
|
||||
|
||||
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
@@ -47,6 +48,8 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
|
||||
protected ?AccompanyingPeriod $period = NULL;
|
||||
|
||||
protected ?int $periodId = NULL;
|
||||
|
||||
/**
|
||||
* Setup before the first test of this class (see phpunit doc)
|
||||
*/
|
||||
@@ -70,15 +73,15 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
*
|
||||
* @dataProvider dataGenerateRandomAccompanyingCourse
|
||||
*/
|
||||
public function testAccompanyingCourseShow(int $personId, AccompanyingPeriod $period)
|
||||
public function testAccompanyingCourseShow(int $personId, int $periodId)
|
||||
{
|
||||
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
|
||||
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
|
||||
|
||||
$data = \json_decode($response->getContent());
|
||||
$this->assertEquals($data->id, $period->getId(),
|
||||
$this->assertEquals($data->id, $periodId,
|
||||
"test that the response's data contains the id of the period"
|
||||
);
|
||||
$this->assertGreaterThan(0, $data->participations);
|
||||
@@ -326,14 +329,16 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
*
|
||||
* @dataProvider dataGenerateRandomAccompanyingCourse
|
||||
*/
|
||||
public function testAccompanyingPeriodPatch(int $personId, AccompanyingPeriod $period)
|
||||
public function testAccompanyingPeriodPatch(int $personId, int $periodId)
|
||||
{
|
||||
$period = self::$container->get(AccompanyingPeriodRepository::class)
|
||||
->find($periodId);
|
||||
$initialValueEmergency = $period->isEmergency();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_PATCH,
|
||||
sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()),
|
||||
sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId),
|
||||
[], // parameters
|
||||
[], // files
|
||||
[], // server parameters
|
||||
@@ -343,11 +348,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$period = $em->getRepository(AccompanyingPeriod::class)
|
||||
->find($period->getId());
|
||||
$em->refresh($period);
|
||||
->find($periodId);
|
||||
$this->assertEquals(!$initialValueEmergency, $period->isEmergency());
|
||||
|
||||
// restore the initial valud
|
||||
// restore the initial value
|
||||
$period->setEmergency($initialValueEmergency);
|
||||
$em->flush();
|
||||
}
|
||||
@@ -361,12 +365,32 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
$em = static::$container->get(EntityManagerInterface::class);
|
||||
$center = $em->getRepository(Center::class)
|
||||
->findOneBy(array('name' => 'Center A'));
|
||||
$qb = $em->createQueryBuilder();
|
||||
|
||||
$personIds = $em->createQuery("SELECT p.id FROM ".
|
||||
Person::class." p ".
|
||||
" WHERE p.center = :center")
|
||||
$personIds = $qb
|
||||
->select('p.id')
|
||||
->distinct(true)
|
||||
->from(Person::class, 'p')
|
||||
->join('p.accompanyingPeriodParticipations', 'participation')
|
||||
->join('participation.accompanyingPeriod', 'ap')
|
||||
->where(
|
||||
$qb->expr()->eq(
|
||||
'p.center',
|
||||
':center'
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->gt(
|
||||
'SIZE(p.accompanyingPeriodParticipations)',
|
||||
0)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->eq('ap.step', ':step')
|
||||
)
|
||||
->setParameter('center', $center)
|
||||
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
|
||||
->setMaxResults($maxResults)
|
||||
->getQuery()
|
||||
->getScalarResult();
|
||||
|
||||
// create a random order
|
||||
@@ -400,11 +424,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
*
|
||||
* @dataProvider dataGenerateRandomAccompanyingCourse
|
||||
*/
|
||||
public function testAccompanyingCourseAddParticipation(int $personId, AccompanyingPeriod $period)
|
||||
public function testAccompanyingCourseAddParticipation(int $personId, int $periodId)
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
|
||||
[], // parameters
|
||||
[], // files
|
||||
[], // server parameters
|
||||
@@ -420,7 +444,8 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
|
||||
// check by deownloading the accompanying cours
|
||||
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
|
||||
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$data = \json_decode($response->getContent());
|
||||
@@ -435,7 +460,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
// check removing the participation
|
||||
$this->client->request(
|
||||
Request::METHOD_DELETE,
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
|
||||
[], // parameters
|
||||
[], // files
|
||||
[], // server parameters
|
||||
@@ -450,42 +475,6 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
$this->assertNotNull($data['startDate']);
|
||||
$this->assertArrayHasKey('endDate', $data);
|
||||
$this->assertNotNull($data['endDate']);
|
||||
|
||||
|
||||
// set to variable for tear down
|
||||
$this->personId = $personId;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
$em = static::$container->get(EntityManagerInterface::class);
|
||||
|
||||
// remove participation if set
|
||||
if ($this->personId && $this->period) {
|
||||
$participation = $em
|
||||
->getRepository(AccompanyingPeriodParticipation::class)
|
||||
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
|
||||
;
|
||||
|
||||
if (NULL !== $participation) {
|
||||
$em->remove($participation);
|
||||
$em->flush();
|
||||
}
|
||||
$this->personId = NULL;
|
||||
$this->period = NULL;
|
||||
} elseif ($this->period) {
|
||||
$period = $em
|
||||
->getRepository(AccompanyingPeriod::class)
|
||||
->find($this->period->getId()) ;
|
||||
|
||||
if ($period !== NULL) {
|
||||
$em->remove($period);
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
$this->period = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
|
||||
@@ -505,12 +494,32 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
$em = static::$container->get(EntityManagerInterface::class);
|
||||
$center = $em->getRepository(Center::class)
|
||||
->findOneBy(array('name' => 'Center A'));
|
||||
$qb = $em->createQueryBuilder();
|
||||
|
||||
$personIds = $em->createQuery("SELECT p.id FROM ".
|
||||
Person::class." p ".
|
||||
" WHERE p.center = :center")
|
||||
$personIds = $qb
|
||||
->select('p.id')
|
||||
->distinct(true)
|
||||
->from(Person::class, 'p')
|
||||
->join('p.accompanyingPeriodParticipations', 'participation')
|
||||
->join('participation.accompanyingPeriod', 'ap')
|
||||
->where(
|
||||
$qb->expr()->eq(
|
||||
'p.center',
|
||||
':center'
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->gt(
|
||||
'SIZE(p.accompanyingPeriodParticipations)',
|
||||
0)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->eq('ap.step', ':step')
|
||||
)
|
||||
->setParameter('center', $center)
|
||||
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
|
||||
->setMaxResults($maxResults)
|
||||
->getQuery()
|
||||
->getScalarResult();
|
||||
|
||||
// create a random order
|
||||
@@ -556,7 +565,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
$qb = $em->createQueryBuilder();
|
||||
$personIds = $qb
|
||||
->select('p.id')
|
||||
->distinct(true)
|
||||
->from(Person::class, 'p')
|
||||
->join('p.accompanyingPeriodParticipations', 'participation')
|
||||
->join('participation.accompanyingPeriod', 'ap')
|
||||
->where(
|
||||
$qb->expr()->eq(
|
||||
'p.center',
|
||||
@@ -568,7 +580,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
'SIZE(p.accompanyingPeriodParticipations)',
|
||||
0)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->eq('ap.step', ':step')
|
||||
)
|
||||
->setParameter('center', $center)
|
||||
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
|
||||
->setMaxResults($maxResults)
|
||||
->getQuery()
|
||||
->getScalarResult();
|
||||
@@ -584,7 +600,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
->find($id);
|
||||
$periods = $person->getAccompanyingPeriods();
|
||||
|
||||
yield [\array_pop($personIds)["id"], $periods[\array_rand($periods)] ];
|
||||
yield [\array_pop($personIds)["id"], $periods[\array_rand($periods)]->getId() ];
|
||||
|
||||
$nbGenerated++;
|
||||
}
|
||||
|
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 Julien Fastré <julien.fastre@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\Tests\Validator;
|
||||
|
||||
use Chill\PersonBundle\Validator\Constraints\BirthdateValidator;
|
||||
use Chill\PersonBundle\Validator\Constraints\Birthdate;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophet;
|
||||
|
||||
/**
|
||||
* Test the behaviour of BirthdayValidator
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class BirthdateValidatorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* A prophecy for \Symfony\Component\Validator\Context\ExecutionContextInterface
|
||||
*
|
||||
* Will reveal \Symfony\Component\Validator\Context\ExecutionContextInterface
|
||||
*/
|
||||
private $context;
|
||||
|
||||
private $prophet;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Birthdate
|
||||
*/
|
||||
private $constraint;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->prophet = new Prophet;
|
||||
|
||||
$constraintViolationBuilder = $this->prophet->prophesize();
|
||||
$constraintViolationBuilder->willImplement('Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface');
|
||||
|
||||
$constraintViolationBuilder->setParameter(Argument::any(), Argument::any())
|
||||
->willReturn($constraintViolationBuilder->reveal());
|
||||
$constraintViolationBuilder->addViolation()
|
||||
->willReturn($constraintViolationBuilder->reveal());
|
||||
|
||||
$this->context = $this->prophet->prophesize();
|
||||
$this->context->willImplement('Symfony\Component\Validator\Context\ExecutionContextInterface');
|
||||
$this->context->buildViolation(Argument::type('string'))
|
||||
->willReturn($constraintViolationBuilder->reveal());
|
||||
|
||||
$this->constraint = new Birthdate();
|
||||
}
|
||||
|
||||
public function testValidBirthDate()
|
||||
{
|
||||
|
||||
$date = new \DateTime('2015-01-01');
|
||||
|
||||
$birthdateValidator = new BirthdateValidator();
|
||||
$birthdateValidator->initialize($this->context->reveal());
|
||||
|
||||
$birthdateValidator->validate($date, $this->constraint);
|
||||
|
||||
$this->context->buildViolation(Argument::any())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testInvalidBirthDate()
|
||||
{
|
||||
$date = new \DateTime('tomorrow');
|
||||
|
||||
$birthdateValidator = new BirthdateValidator();
|
||||
$birthdateValidator->initialize($this->context->reveal());
|
||||
|
||||
$birthdateValidator->validate($date, $this->constraint);
|
||||
|
||||
$this->context->buildViolation(Argument::type('string'))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testInvalidBirthDateWithParameter()
|
||||
{
|
||||
$date = (new \DateTime('today'))->sub(new \DateInterval('P1M'));
|
||||
|
||||
$birthdateValidator = new BirthdateValidator('P1Y');
|
||||
$birthdateValidator->initialize($this->context->reveal());
|
||||
|
||||
$birthdateValidator->validate($date, $this->constraint);
|
||||
|
||||
$this->context->buildViolation(Argument::type('string'))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$this->prophet->checkPredictions();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 Julien Fastré <julien.fastre@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\Tests\Validator\Person;
|
||||
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\BirthdateValidator;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
|
||||
/**
|
||||
* Test the behaviour of BirthdayValidator
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class BirthdateValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
|
||||
public function testValidateTodayInvalid()
|
||||
{
|
||||
$bornToday = new \DateTime('now');
|
||||
$this->validator->validate($bornToday, $this->createConstraint());
|
||||
$this->buildViolation('msg')
|
||||
->setParameter('%date%', (new \DateTime('yesterday'))->format('d-m-Y'))
|
||||
->setCode('3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4')
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
public function testValidateYesterdayValid()
|
||||
{
|
||||
$bornYesterday = new \DateTime('yesterday');
|
||||
$this->validator->validate($bornYesterday, $this->createConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testTomorrowInvalid()
|
||||
{
|
||||
$bornAfter = new \DateTime('+2 days');
|
||||
$this->validator->validate($bornAfter, $this->createConstraint());
|
||||
$this->buildViolation('msg')
|
||||
->setParameter('%date%', (new \DateTime('yesterday'))->format('d-m-Y'))
|
||||
->setCode('3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4')
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
private function createConstraint()
|
||||
{
|
||||
return new Birthdate([
|
||||
'message' => 'msg'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
return new BirthdateValidator('P1D');
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Validator\Person;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenterValidator;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
class PersonHasCenterValidatorTest extends \Symfony\Component\Validator\Test\ConstraintValidatorTestCase
|
||||
{
|
||||
public function testValidateRequired()
|
||||
{
|
||||
$constraint = $this->getConstraint();
|
||||
$personHasCenter = (new Person())->setCenter(new Center());
|
||||
$personNoCenter = new Person();
|
||||
|
||||
$this->validator->validate($personHasCenter, $constraint);
|
||||
$this->assertNoViolation();
|
||||
|
||||
$this->validator->validate($personNoCenter, $constraint);
|
||||
$this->buildViolation('msg')
|
||||
->atPath('property.path.center')
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
protected function getConstraint()
|
||||
{
|
||||
return new PersonHasCenter([
|
||||
'message' => 'msg'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
$parameterBag = $this->createMock(ParameterBagInterface::class);
|
||||
$parameterBag
|
||||
->method('get')
|
||||
->with($this->equalTo('chill_person'))
|
||||
->willReturn([
|
||||
'validation' => [
|
||||
'center_required' => true
|
||||
]
|
||||
])
|
||||
;
|
||||
|
||||
return new PersonHasCenterValidator($parameterBag);
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Validator\Person;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
class PersonValidationTest extends KernelTestCase
|
||||
{
|
||||
private ValidatorInterface $validator;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->validator = self::$container->get(ValidatorInterface::class);
|
||||
}
|
||||
|
||||
public function testFirstnameValidation()
|
||||
{
|
||||
$person = (new Person())
|
||||
->setFirstname(\str_repeat('a', 500));
|
||||
$errors = $this->validator->validate($person, null, ["creation"]);
|
||||
foreach ($errors->getIterator() as $error) {
|
||||
if (Length::TOO_LONG_ERROR === $error->getCode()) {
|
||||
$this->assertTrue(true,
|
||||
"error code for firstname too long is present");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->assertTrue(false,
|
||||
"error code for fistname too long is present");
|
||||
|
||||
}
|
||||
|
||||
public function testBirthdateInFuture()
|
||||
{
|
||||
$person = (new Person())
|
||||
->setBirthdate(new \Datetime('+2 months'));
|
||||
$errors = $this->validator->validate($person, null, ["creation"]);
|
||||
foreach ($errors->getIterator() as $error) {
|
||||
if (Birthdate::BIRTHDATE_INVALID_CODE === $error->getCode()) {
|
||||
$this->assertTrue(true,
|
||||
"error code for birthdate invalid is present");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->assertTrue(false,
|
||||
"error code for birthdate invalid is present");
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -17,26 +17,23 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints;
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Person;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Create a constraint on birth date: the birthdate after today are not allowed.
|
||||
*
|
||||
* It is possible to add a delay before today, expressed as described in
|
||||
*
|
||||
* It is possible to add a delay before today, expressed as described in
|
||||
* interval_spec : http://php.net/manual/en/dateinterval.construct.php
|
||||
* (this interval_spec itself is based on ISO8601 :
|
||||
* https://en.wikipedia.org/wiki/ISO_8601#Durations)
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
* @Annotation
|
||||
*/
|
||||
class Birthdate extends Constraint
|
||||
{
|
||||
public const BIRTHDATE_INVALID_CODE = '3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4';
|
||||
|
||||
public $message = "The birthdate must be before %date%";
|
||||
|
||||
public function validatedBy()
|
||||
{
|
||||
return 'birthdate_not_before';
|
||||
}
|
||||
}
|
@@ -17,49 +17,50 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints;
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Person;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class BirthdateValidator extends ConstraintValidator
|
||||
{
|
||||
private $interval_spec = null;
|
||||
|
||||
|
||||
public function __construct($interval_spec = null)
|
||||
{
|
||||
$this->interval_spec = $interval_spec;
|
||||
}
|
||||
|
||||
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if ($value === NULL) {
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \LogicException('The input should a be a \DateTime interface,'
|
||||
. (is_object($value) ? get_class($value) : gettype($value)));
|
||||
}
|
||||
|
||||
|
||||
$limitDate = $this->getLimitDate();
|
||||
|
||||
|
||||
if ($limitDate < $value) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('%date%', $limitDate->format('d-m-Y'))
|
||||
->setCode(Birthdate::BIRTHDATE_INVALID_CODE)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
private function getLimitDate()
|
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Person;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
class PersonHasCenter extends \Symfony\Component\Validator\Constraint
|
||||
{
|
||||
public string $message = "A center is required";
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [
|
||||
self::CLASS_CONSTRAINT
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Person;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class PersonHasCenterValidator extends \Symfony\Component\Validator\ConstraintValidator
|
||||
{
|
||||
private bool $centerRequired;
|
||||
|
||||
public function __construct(ParameterBagInterface $parameterBag)
|
||||
{
|
||||
$this->centerRequired = $parameterBag->get('chill_person')['validation']['center_required'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate($person, Constraint $constraint)
|
||||
{
|
||||
if (!$person instanceof Person) {
|
||||
throw new UnexpectedTypeException($constraint, Person::class);
|
||||
}
|
||||
|
||||
if (!$this->centerRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (NULL === $person->getCenter()) {
|
||||
$this
|
||||
->context
|
||||
->buildViolation($constraint->message)
|
||||
->atPath('center')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,14 +24,6 @@ services:
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
chill.person.form.type.select2maritalstatus:
|
||||
class: Chill\PersonBundle\Form\Type\Select2MaritalStatusType
|
||||
arguments:
|
||||
- "@request_stack"
|
||||
- "@doctrine.orm.entity_manager"
|
||||
tags:
|
||||
- { name: form.type, alias: select2_chill_marital_status }
|
||||
|
||||
chill.person.timeline.accompanying_period_opening:
|
||||
class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodOpening
|
||||
arguments:
|
||||
@@ -62,23 +54,22 @@ services:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
chill.person.birthdate_validation:
|
||||
class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator
|
||||
Chill\PersonBundle\Validator\Constraints\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Validator/Constraints/'
|
||||
|
||||
# override default config, must be loaded after resource
|
||||
Chill\PersonBundle\Validator\Constraints\BirthdateValidator:
|
||||
arguments:
|
||||
- "%chill_person.validation.birtdate_not_before%"
|
||||
tags:
|
||||
- { name: validator.constraint_validator, alias: birthdate_not_before }
|
||||
|
||||
Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequentialValidator:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: validator.constraint_validator }
|
||||
|
||||
Chill\PersonBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Repository/'
|
||||
tags: ['doctrine.repository_service']
|
||||
|
||||
Chill\PersonBundle\Controller\:
|
||||
autowire: true
|
||||
|
@@ -12,9 +12,8 @@ services:
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Chill\PersonBundle\Controller\AccompanyingPeriodController:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
|
||||
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Chill\PersonBundle\Controller\PersonAddressController:
|
||||
|
@@ -1,5 +1,10 @@
|
||||
services:
|
||||
|
||||
Chill\PersonBundle\Form\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Form/'
|
||||
|
||||
Chill\PersonBundle\Form\PersonType:
|
||||
arguments:
|
||||
- '%chill_person.person_fields%'
|
||||
@@ -7,14 +12,7 @@ services:
|
||||
tags:
|
||||
- { name: form.type, alias: '@chill.person.form.person_creation' }
|
||||
|
||||
Chill\PersonBundle\Form\CreationPersonType:
|
||||
arguments:
|
||||
- '@chill.main.form.data_transformer.center_transformer'
|
||||
- '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
|
||||
tags:
|
||||
- { name: form.type, alias: '@chill.main.form.person_creation' }
|
||||
|
||||
chill.person.accompanying_period_closing_motive:
|
||||
Chill\PersonBundle\Form\Type\ClosingMotivePickerType:
|
||||
class: Chill\PersonBundle\Form\Type\ClosingMotivePickerType
|
||||
arguments:
|
||||
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
|
||||
@@ -28,60 +26,3 @@ services:
|
||||
$config: "%chill_person.accompanying_period_fields%"
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
chill.person.form.type.pick_person:
|
||||
class: Chill\PersonBundle\Form\Type\PickPersonType
|
||||
arguments:
|
||||
- '@Chill\PersonBundle\Repository\PersonRepository'
|
||||
- "@security.token_storage"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
|
||||
- '@Symfony\Component\Translation\TranslatorInterface'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
Chill\PersonBundle\Form\Type\PersonAltNameType:
|
||||
arguments:
|
||||
$configHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
Chill\PersonBundle\Form\Type\PersonPhoneType:
|
||||
arguments:
|
||||
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
Chill\PersonBundle\Form\SocialWork\SocialIssueType:
|
||||
arguments:
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
Chill\PersonBundle\Form\SocialWork\SocialActionType:
|
||||
arguments:
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
Chill\PersonBundle\Form\SocialWork\EvaluationType:
|
||||
arguments:
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
|
||||
Chill\PersonBundle\Form\SocialWork\GoalType:
|
||||
arguments:
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
|
||||
Chill\PersonBundle\Form\SocialWork\ResultType:
|
||||
arguments:
|
||||
$translatableStringHelper: '@chill.main.helper.translatable_string'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
@@ -19,14 +19,7 @@ services:
|
||||
# - { name: 'chill.menu_builder' }
|
||||
#
|
||||
Chill\PersonBundle\Menu\PersonMenuBuilder:
|
||||
arguments:
|
||||
$showAccompanyingPeriod: '%chill_person.accompanying_period%'
|
||||
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
||||
|
||||
# Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
|
||||
# arguments:
|
||||
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
||||
# tags:
|
||||
# - { name: 'chill.menu_builder' }
|
||||
|
@@ -1,6 +1,12 @@
|
||||
services:
|
||||
# note: the services.yaml file define some autoloading
|
||||
|
||||
chill.person.repository.person:
|
||||
class: Chill\PersonBundle\Repository\PersonRepository
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
Chill\PersonBundle\Repository\PersonRepository: '@chill.person.repository.person'
|
||||
|
||||
Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\PersonACLAwareRepository'
|
||||
|
||||
Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository'
|
||||
|
@@ -1,25 +1,11 @@
|
||||
services:
|
||||
chill.person.search_person:
|
||||
class: Chill\PersonBundle\Search\PersonSearch
|
||||
arguments:
|
||||
- "@doctrine.orm.entity_manager"
|
||||
- "@security.token_storage"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- "@chill_main.paginator_factory"
|
||||
calls:
|
||||
- ['setContainer', ["@service_container"]]
|
||||
Chill\PersonBundle\Search\PersonSearch:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_regular' }
|
||||
|
||||
Chill\PersonBundle\Search\SimilarityPersonSearch:
|
||||
arguments:
|
||||
- "@doctrine.orm.entity_manager"
|
||||
- "@security.token_storage"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- "@chill_main.paginator_factory"
|
||||
- '@chill.person.search_person'
|
||||
calls:
|
||||
- ['setContainer', ["@service_container"]]
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_similarity' }
|
||||
|
||||
|
@@ -1,16 +1,14 @@
|
||||
services:
|
||||
chill.person.security.authorization.person:
|
||||
autowire: true
|
||||
class: Chill\PersonBundle\Security\Authorization\PersonVoter
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
|
||||
|
@@ -1,70 +1,3 @@
|
||||
Chill\PersonBundle\Entity\Person:
|
||||
properties:
|
||||
firstName:
|
||||
- NotBlank:
|
||||
groups: [general, creation]
|
||||
- Length:
|
||||
min: 2
|
||||
max: 255
|
||||
minMessage: 'This name is too short. It must containt {{ limit }} chars'
|
||||
maxMessage: 'This name is too long. It must containt {{ limit }} chars'
|
||||
groups: [general, creation]
|
||||
lastName:
|
||||
- NotBlank:
|
||||
groups: [general, creation]
|
||||
- Length:
|
||||
min: 2
|
||||
max: 255
|
||||
minMessage: 'This name is too short. It must containt {{ limit }} chars'
|
||||
maxMessage: 'This name is too long. It must containt {{ limit }} chars'
|
||||
groups: [general, creation]
|
||||
birthdate:
|
||||
- Date:
|
||||
message: 'Birthdate not valid'
|
||||
groups: [general, creation]
|
||||
- Chill\PersonBundle\Validator\Constraints\Birthdate:
|
||||
groups: [general, creation]
|
||||
gender:
|
||||
- NotNull:
|
||||
groups: [general, creation]
|
||||
#accompanyingPeriods:
|
||||
# - Valid:
|
||||
# traverse: true
|
||||
email:
|
||||
- Email:
|
||||
groups: [general, creation]
|
||||
message: 'The email is not valid'
|
||||
checkMX: true
|
||||
phonenumber:
|
||||
- Regex:
|
||||
pattern: '/^([\+{1}])([0-9\s*]{4,20})$/'
|
||||
groups: [general, creation]
|
||||
message: 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33123456789'
|
||||
- Chill\MainBundle\Validation\Constraint\PhonenumberConstraint:
|
||||
type: landline
|
||||
groups: [ general, creation ]
|
||||
mobilenumber:
|
||||
- Regex:
|
||||
pattern: '/^([\+{1}])([0-9\s*]{4,20})$/'
|
||||
groups: [general, creation]
|
||||
message: 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789'
|
||||
- Chill\MainBundle\Validation\Constraint\PhonenumberConstraint:
|
||||
type: mobile
|
||||
groups: [ general, creation ]
|
||||
otherPhoneNumbers:
|
||||
- Valid:
|
||||
traverse: true
|
||||
constraints:
|
||||
- Callback:
|
||||
callback: isAccompanyingPeriodValid
|
||||
groups: [accompanying_period_consistent]
|
||||
- Callback:
|
||||
callback: isAddressesValid
|
||||
groups: [addresses_consistent]
|
||||
- Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential:
|
||||
groups: [ 'household_memberships' ]
|
||||
|
||||
|
||||
Chill\PersonBundle\Entity\AccompanyingPeriod:
|
||||
properties:
|
||||
openingDate:
|
||||
|
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Allow to create persons without center
|
||||
*/
|
||||
final class Version20210831140339 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Allow to create persons without center';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER center_id DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER center_id SET NOT NULL');
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Optimize trigram index on person fullname: create index for both center_id and fullname
|
||||
*/
|
||||
final class Version20210910161858 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Optimize trigram index on person fullname: create index for both center_id and fullname';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX fullnamecanonical_trgm_idx');
|
||||
$this->addSql('CREATE INDEX fullnameCanonical_trgm_idx ON chill_person_person USING GIST (center_id, fullnameCanonical gist_trgm_ops)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX fullnamecanonical_trgm_idx');
|
||||
$this->addSql('CREATE INDEX fullnameCanonical_trgm_idx ON chill_person_person USING GIST (fullnameCanonical gist_trgm_ops)');
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create view for PersonCurrentAddress and related indexes
|
||||
*/
|
||||
final class Version20210915093624 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create view for PersonCurrentAddress and related indexes';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("CREATE 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)
|
||||
");
|
||||
|
||||
$this->addSql("CREATE INDEX chill_custom_main_address_filtering_idx ON chill_main_address USING btree (id, validfrom ASC, validto DESC)");
|
||||
$this->addSql("CREATE INDEX chill_custom_person_household_members_sharing_idx ON chill_person_household_members USING btree (person_id, startdate ASC, enddate DESC, household_id) WHERE sharedhousehold IS TRUE");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql("DROP VIEW view_chill_person_current_address");
|
||||
$this->addSql("DROP INDEX chill_custom_main_address_filtering_idx");
|
||||
$this->addSql("DROP INDEX chill_custom_person_household_members_sharing_idx");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user