Merge branch 'master' into '37_modal_add-persons'

# Conflicts:
#   src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
#   src/Bundle/ChillPersonBundle/chill.webpack.config.js
This commit is contained in:
2021-05-07 13:35:48 +00:00
61 changed files with 3368 additions and 262 deletions

View File

@@ -0,0 +1,79 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
class AccompanyingCourseApiController extends ApiController
{
protected EventDispatcherInterface $eventDispatcher;
protected ValidatorInterface $validator;
public function __construct(EventDispatcherInterface $eventDispatcher, $validator)
{
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
}
public function participationApi($id, Request $request, $_format)
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
$person = $this->getSerializer()
->deserialize($request->getContent(), Person::class, $_format, []);
if (NULL === $person) {
throw new BadRequestException('person id not found');
}
// TODO add acl
//
$this->onPostCheckACL('participation', $request, $_format, $accompanyingPeriod);
switch ($request->getMethod()) {
case Request::METHOD_POST:
$participation = $accompanyingPeriod->addPerson($person);
break;
case Request::METHOD_DELETE:
$participation = $accompanyingPeriod->removePerson($person);
$participation->setEndDate(new \DateTimeImmutable('now'));
break;
default:
throw new BadRequestException("This method is not supported");
}
$errors = $this->validator->validate($accompanyingPeriod);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($participation);
}
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response
{
$this->eventDispatcher->dispatch(
AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT,
new AccompanyingPeriodPrivacyEvent($entity, [
'action' => $action,
'request' => $request->getMethod()
])
);
return null;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class OpeningApiController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $qb): void
{
$qb->where($qb->expr()->gt('e.noActiveAfter', ':now'))
->orWhere($qb->expr()->isNull('e.noActiveAfter'));
$qb->setParameter('now', new \DateTime('now'));
}
}

View File

@@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Person;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Doctrine\Model\Point;
/**
* Load people into database
@@ -37,17 +38,17 @@ use Chill\MainBundle\Entity\Address;
* @author Marc Ducobu <marc@champs-libres.coop>
*/
class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
{
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
protected $faker;
public function __construct()
{
$this->faker = \Faker\Factory::create('fr_FR');
}
public function prepare()
{
//prepare days, month, years
@@ -56,57 +57,57 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
$this->years[] = $y;
$y = $y +1;
} while ($y >= 1990);
$m = 1;
do {
$this->month[] = $m;
$m = $m +1;
} while ($m >= 12);
$d = 1;
do {
$this->day[] = $d;
$d = $d + 1;
} while ($d <= 28);
}
public function getOrder()
{
return 10000;
}
public function load(ObjectManager $manager)
{
$this->loadRandPeople($manager);
$this->loadExpectedPeople($manager);
$manager->flush();
}
public function loadExpectedPeople(ObjectManager $manager)
{
echo "loading expected people...\n";
foreach ($this->peoples as $person) {
$this->addAPerson($this->fillWithDefault($person), $manager);
}
}
public function loadRandPeople(ObjectManager $manager)
{
echo "loading rand people...\n";
$this->prepare();
$chooseLastNameOrTri = array('tri', 'tri', 'name', 'tri');
$i = 0;
do {
$i++;
$sex = $this->genders[array_rand($this->genders)];
if ($chooseLastNameOrTri[array_rand($chooseLastNameOrTri)] === 'tri' ) {
$length = rand(2, 3);
$lastName = '';
@@ -117,13 +118,13 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
} else {
$lastName = $this->lastNames[array_rand($this->lastNames)];
}
if ($sex === Person::MALE_GENDER) {
$firstName = $this->firstNamesMale[array_rand($this->firstNamesMale)];
} else {
$firstName = $this->firstNamesFemale[array_rand($this->firstNamesFemale)];
}
// add an address on 80% of the created people
if (rand(0,100) < 80) {
$address = $this->getRandomAddress();
@@ -137,7 +138,7 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
} else {
$address = null;
}
$person = array(
'FirstName' => $firstName,
'LastName' => $lastName,
@@ -147,15 +148,15 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
'Address' => $address,
'maritalStatus' => $this->maritalStatusRef[array_rand($this->maritalStatusRef)]
);
$this->addAPerson($this->fillWithDefault($person), $manager);
} while ($i <= 100);
}
/**
* fill a person array with default value
*
*
* @param string[] $specific
*/
private function fillWithDefault(array $specific)
@@ -171,10 +172,10 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
'Address' => null
), $specific);
}
/**
* create a new person from array data
*
*
* @param array $person
* @param ObjectManager $manager
* @throws \Exception
@@ -200,35 +201,51 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
$this->addAccompanyingPeriods($p, $value, $manager);
break;
}
//try to add the data using the setSomething function,
// if not possible, fallback to addSomething function
if (method_exists($p, 'set'.$key)) {
call_user_func(array($p, 'set'.$key), $value);
} elseif (method_exists($p, 'add'.$key)) {
// if we have a "addSomething", we may have multiple items to add
// so, we set the value in an array if it is not an array, and
// so, we set the value in an array if it is not an array, and
// will call the function addSomething multiple times
if (!is_array($value)) {
$value = array($value);
}
foreach($value as $v) {
if ($v !== NULL) {
call_user_func(array($p, 'add'.$key), $v);
}
}
}
}
}
$manager->persist($p);
echo "add person'".$p->__toString()."'\n";
}
/**
* Creata a random address
*
* Create a random point
*
* @return Point
*/
private function getRandomPoint()
{
$lonBrussels = 4.35243;
$latBrussels = 50.84676;
$lon = $lonBrussels + 0.01 * rand(-5, 5);
$lat = $latBrussels + 0.01 * rand(-5, 5);
return Point::fromLonLat($lon, $lat);
}
/**
* Create a random address
*
* @return Address
*/
private function getRandomAddress()
@@ -238,13 +255,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
->setStreetAddress2(
rand(0,9) > 5 ? $this->faker->streetAddress : ''
)
->setPoint(
rand(0,9) > 5 ? $this->getRandomPoint() : NULL
)
->setPostcode($this->getReference(
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)]
))
->setValidFrom($this->faker->dateTimeBetween('-5 years'))
;
}
private function getCountry($countryCode)
{
if ($countryCode === NULL) {
@@ -257,30 +277,30 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
private $maritalStatusRef = ['ms_single', 'ms_married', 'ms_widow', 'ms_separat',
'ms_divorce', 'ms_legalco', 'ms_unknown'];
private $firstNamesMale = array("Jean", "Mohamed", "Alfred", "Robert", "Justin", "Brian",
"Compère", "Jean-de-Dieu", "Charles", "Pierre", "Luc", "Mathieu", "Alain", "Etienne", "Eric",
"Corentin", "Gaston", "Spirou", "Fantasio", "Mahmadou", "Mohamidou", "Vursuv", "Youssef" );
private $firstNamesFemale = array("Svedana", "Sevlatina", "Irène", "Marcelle",
"Corentine", "Alfonsine", "Caroline", "Solange", "Gostine", "Fatoumata", "Nicole",
"Groseille", "Chana", "Oxana", "Ivana", "Julie", "Tina", "Adèle" );
private $lastNames = array("Diallo", "Bah", "Gaillot", "Martin");
private $lastNamesTrigrams = array("fas", "tré", "hu", 'blart', 'van', 'der', 'lin', 'den',
'ta', 'mi', 'net', 'gna', 'bol', 'sac', 'ré', 'jo', 'du', 'pont', 'cas', 'tor', 'rob', 'al',
'ma', 'gone', 'car',"fu", "ka", "lot", "no", "va", "du", "bu", "su", "jau", "tte", 'sir',
"lo", 'to', "cho", "car", 'mo','zu', 'qi', 'mu');
private $genders = array(Person::MALE_GENDER, Person::FEMALE_GENDER);
private $years = array();
private $month = array();
private $day = array();
private $peoples = array(
array(
'LastName' => "Depardieu",
@@ -362,21 +382,21 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
'maritalStatus' => 'ms_legalco'
),
);
private function addAccompanyingPeriods(Person $person, array $periods, ObjectManager $manager)
{
foreach ($periods as $period) {
echo "adding new past Accompanying Period..\n";
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = new AccompanyingPeriod(new \DateTime($period['from']));
$accompanyingPeriod
->setClosingDate(new \DateTime($period['to']))
->setRemark($period['remark'])
;
$person->addAccompanyingPeriod($accompanyingPeriod);
}
}

View File

@@ -25,6 +25,7 @@ use Doctrine\Persistence\ObjectManager;
use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup;
use Chill\MainBundle\Entity\RoleScope;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
/**
* Add a role CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE for all groups except administrative,
@@ -44,6 +45,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
{
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
$permissionsGroup = $this->getReference($permissionsGroupRef);
$scopeSocial = $this->getReference('scope_social');
//create permission group
switch ($permissionsGroup->getName()) {
@@ -51,6 +53,12 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
case 'direction':
printf("Adding CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE to %s permission group \n", $permissionsGroup->getName());
$permissionsGroup->addRoleScope(
(new RoleScope())
->setRole(AccompanyingPeriodVoter::SEE)
->setScope($scopeSocial)
);
$roleScopeUpdate = (new RoleScope())
->setRole('CHILL_PERSON_UPDATE')
->setScope(null);

View File

@@ -28,6 +28,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ChillPersonExtension
@@ -76,6 +77,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/templating.yaml');
$loader->load('services/alt_names.yaml');
$loader->load('services/serializer.yaml');
$loader->load('services/security.yaml');
// load service advanced search only if configure
if ($config['search']['search_by_phone'] != 'never') {
@@ -307,6 +309,55 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'template' => '@ChillPerson/MaritalStatus/edit.html.twig',
]
]
],
],
'apis' => [
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod::class,
'name' => 'accompanying_course',
'base_path' => '/api/1.0/person/accompanying-course',
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'participation' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
]
]
],
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class,
'name' => 'accompanying_period_origin',
'base_path' => '/api/1.0/person/accompanying-period/origin',
'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class,
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
]
]
]);

View File

@@ -118,7 +118,7 @@ class AccompanyingPeriod
*
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
* mappedBy="accompanyingPeriod",
* cascade={"persist", "remove", "merge", "detach"})
* cascade={"persist", "refresh", "remove", "merge", "detach"})
*/
private $participations;
@@ -344,26 +344,43 @@ class AccompanyingPeriod
}
/**
* This private function scan Participations Collection,
* searching for a given Person
* Get the participation containing a person
*/
private function participationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation
public function getParticipationsContainsPerson(Person $person): Collection
{
foreach ($this->participations as $participation) {
/** @var AccompanyingPeriodParticipation $participation */
if ($person === $participation->getPerson() && $participation->getEndDate() === NULL) {
return $participation;
}}
return null;
return $this->getParticipations($person)->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if ($person === $participation->getPerson()) {
return $participation;
}
});
}
/**
* This public function is the same but return only true or false
* Get the opened participation containing a person
*
* "Open" means that the closed date is NULL
*/
public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation
{
$collection = $this->getParticipationsContainsPerson($person)->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if (NULL === $participation->getEndDate()) {
return $participation;
}
});
return $collection->count() > 0 ? $collection->first() : NULL;
}
/**
* Return true if the accompanying period contains a person.
*
* **Note**: this participation can be opened or not.
*/
public function containsPerson(Person $person): bool
{
return ($this->participationsContainsPerson($person) === null) ? false : true;
return $this->getParticipationsContainsPerson($person)->count() > 0;
}
/**
@@ -380,14 +397,14 @@ class AccompanyingPeriod
/**
* Remove Person
*/
public function removePerson(Person $person): AccompanyingPeriodParticipation
public function removePerson(Person $person): ?AccompanyingPeriodParticipation
{
$participation = $this->participationsContainsPerson($person);
$participation = $this->getOpenParticipationContainsPerson($person);
if (! null === $participation) {
if ($participation instanceof AccompanyingPeriodParticipation) {
$participation->setEndDate(new \DateTimeImmutable('now'));
}
return $participation;
}

View File

@@ -22,7 +22,7 @@
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository;
use Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository;
use Doctrine\ORM\Mapping as ORM;
/**
@@ -53,7 +53,7 @@ class Origin
return $this->id;
}
public function getLabel(): ?string
public function getLabel()
{
return $this->label;
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Chill\MainBundle\Entity\Address;
/**
* @ORM\Entity(repositoryClass=HouseholdRepository::class)
*/
class Household
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
public function getId(): ?int
{
return $this->id;
}
/**
* Addresses
* @var Collection
*
* @ORM\ManyToMany(
* targetEntity="Chill\MainBundle\Entity\Address",
* cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
*/
private $addresses;
/**
* @param Address $address
* @return $this
*/
public function addAddress(Address $address)
{
$this->addresses[] = $address;
return $this;
}
/**
* @param Address $address
*/
public function removeAddress(Address $address)
{
$this->addresses->removeElement($address);
}
/**
* By default, the addresses are ordered by date, descending (the most
* recent first)
*
* @return \Chill\MainBundle\Entity\Address[]
*/
public function getAddresses()
{
return $this->addresses;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Repository\Household\HouseholdMembersRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
/**
* @ORM\Entity(repositoryClass=HouseholdMembersRepository::class)
*/
class HouseholdMembers
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $position;
/**
* @ORM\Column(type="date")
*/
private $startDate;
/**
* @ORM\Column(type="date")
*/
private $endDate;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $comment;
/**
* @ORM\Column(type="boolean")
*/
private $sharedHousehold;
/**
*
* @var Person
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
*/
private $person;
/**
*
* @var Household
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Household\Household"
* )
*/
private $household;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): ?string
{
return $this->position;
}
public function setPosition(?string $position): self
{
$this->position = $position;
return $this;
}
public function getStartDate(): ?\DateTimeInterface
{
return $this->startDate;
}
public function setStartDate(\DateTimeInterface $startDate): self
{
$this->startDate = $startDate;
return $this;
}
public function getEndDate(): ?\DateTimeInterface
{
return $this->endDate;
}
public function setEndDate(\DateTimeInterface $endDate): self
{
$this->endDate = $endDate;
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(?string $comment): self
{
$this->comment = $comment;
return $this;
}
public function getSharedHousehold(): ?bool
{
return $this->sharedHousehold;
}
public function setSharedHousehold(bool $sharedHousehold): self
{
$this->sharedHousehold = $sharedHousehold;
return $this;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function setPerson(?Person $person): self
{
$this->person = $person;
return $this;
}
public function getHousehold(): ?Household
{
return $this->household;
}
public function setHousehold(?Household $household): self
{
$this->household = $household;
return $this;
}
}

View File

@@ -390,7 +390,7 @@ class Person implements HasCenterInterface
*
* @deprecated since 1.1 use `getOpenedAccompanyingPeriod instead
*/
public function getCurrentAccompanyingPeriod() : AccompanyingPeriod
public function getCurrentAccompanyingPeriod() : ?AccompanyingPeriod
{
return $this->getOpenedAccompanyingPeriod();
}
@@ -1011,7 +1011,7 @@ class Person implements HasCenterInterface
* By default, the addresses are ordered by date, descending (the most
* recent first)
*/
public function getAddresses(): ArrayCollection
public function getAddresses(): Collection
{
return $this->addresses;
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMembers;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method HouseholdMembers|null find($id, $lockMode = null, $lockVersion = null)
* @method HouseholdMembers|null findOneBy(array $criteria, array $orderBy = null)
* @method HouseholdMembers[] findAll()
* @method HouseholdMembers[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class HouseholdMembersRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, HouseholdMembers::class);
}
// /**
// * @return HouseholdMembers[] Returns an array of HouseholdMembers objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('h')
->andWhere('h.exampleField = :val')
->setParameter('val', $value)
->orderBy('h.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?HouseholdMembers
{
return $this->createQueryBuilder('h')
->andWhere('h.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Household;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Household|null find($id, $lockMode = null, $lockVersion = null)
* @method Household|null findOneBy(array $criteria, array $orderBy = null)
* @method Household[] findAll()
* @method Household[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class HouseholdRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Household::class);
}
// /**
// * @return Household[] Returns an array of Household objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('h')
->andWhere('h.exampleField = :val')
->setParameter('val', $value)
->orderBy('h.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Household
{
return $this->createQueryBuilder('h')
->andWhere('h.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View File

@@ -21,17 +21,17 @@
{% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
{% block personcontent %}
<h1>{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{{ form_start(form) }}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.streetAddress1) }}
{{ form_row(form.streetAddress2) }}
{{ form_row(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_row(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
@@ -42,7 +42,7 @@
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-save' }, 'label': 'Save' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock personcontent %}
{% endblock personcontent %}

View File

@@ -23,9 +23,9 @@
{% block title %}{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}{% endblock %}
{% block personcontent %}
<h1>{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
<table class="records_list">
<thead>
<tr>
@@ -48,11 +48,11 @@
{% for address in person.addresses %}
<tr>
<td><strong>{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }}</strong></td>
<td>
{{ address_macros._render(address, { 'with_valid_from' : false, 'has_no_address': true } ) }}
</td>
<td>
<ul class="record_actions">
<li>
@@ -61,11 +61,17 @@
</ul>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
<ul class="record_actions">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
@@ -73,10 +79,11 @@
</a>
</li>
<li>
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}" class="sc-button bt-create">
{{ 'Add an address'|trans }}
</a>
</li>
</ul>
{% endblock personcontent %}
{% endblock personcontent %}

View File

@@ -21,21 +21,21 @@
{% block title %}{{ 'New address for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %}
{% block personcontent %}
<h1>{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{{ form_start(form) }}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.streetAddress1) }}
{{ form_errors(form.streetAddress1) }}
{{ form_row(form.streetAddress2) }}
{{ form_errors(form.streetAddress2) }}
{{ form_row(form.street) }}
{{ form_errors(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_errors(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_errors(form.postCode) }}
{{ form_row(form.validFrom) }}
{{ form_errors(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
@@ -46,7 +46,7 @@
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock personcontent %}
{% endblock personcontent %}

View File

@@ -0,0 +1,73 @@
<?php
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\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;
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
protected AuthorizationHelper $helper;
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
/**
* @param AuthorizationHelper $helper
*/
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
}
protected function supports($attribute, $subject)
{
return $subject instanceof AccompanyingPeriod;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
if (!$token->getUser() instanceof User) {
return false;
}
// TODO take scopes into account
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;
}
return false;
}
}
private function getAttributes()
{
return [
self::SEE
];
}
public function getRoles()
{
return $this->getAttributes();
}
public function getRolesWithoutScope()
{
return [];
}
public function getRolesWithHierarchy()
{
return [ 'Person' => $this->getRoles() ];
}
}

View File

@@ -55,7 +55,7 @@ class PersonNormalizer implements
'id' => $person->getId(),
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'birthdate' => $person->getBirthdate() ? $this->normalizer->normalize($person->getBirthdate()) : null,
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
'center' => $this->normalizer->normalize($person->getCenter())
];
}

View File

@@ -36,7 +36,7 @@ use Symfony\Component\HttpFoundation\Request;
/**
* Test api for AccompanyingCourseControllerTest
*/
class AccompanyingCourseControllerTest extends WebTestCase
class AccompanyingCourseApiControllerTest extends WebTestCase
{
protected static EntityManagerInterface $em;
@@ -65,7 +65,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
*/
public function testAccompanyingCourseShow(int $personId, AccompanyingPeriod $period)
{
$this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId()));
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
@@ -77,6 +77,14 @@ class AccompanyingCourseControllerTest extends WebTestCase
$this->assertGreaterThan(0, $data->participations);
}
public function testShow404()
{
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', 99999));
$response = $this->client->getResponse();
$this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)");
}
/**
*
* @dataProvider dataGenerateRandomAccompanyingCourse
@@ -85,26 +93,55 @@ class AccompanyingCourseControllerTest extends WebTestCase
{
$this->client->request(
Request::METHOD_POST,
sprintf('/fr/person/api/1.0/accompanying-course/%d/participation.json', $period->getId()),
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
$this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId()));
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('startDate', $data);
$this->assertNotNull($data['startDate']);
// check by deownloading the accompanying cours
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
$response = $this->client->getResponse();
$data = \json_decode($response->getContent());
// check that the person id is contained
$participationsPersonsIds = \array_map(
function($participation) { return $participation->person->id; },
$data->participations);
$this->assertContains($personId, $participationsPersonsIds);
// check removing the participation
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('startDate', $data);
$this->assertNotNull($data['startDate']);
$this->assertArrayHasKey('endDate', $data);
$this->assertNotNull($data['endDate']);
// set to variable for tear down
$this->personId = $personId;
$this->period = $period;
}
@@ -112,6 +149,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
protected function tearDown()
{
// remove participation created during test 'testAccompanyingCourseAddParticipation'
// and if the test could not remove it
$testAddParticipationName = 'testAccompanyingCourseAddParticipation';
@@ -126,8 +164,10 @@ class AccompanyingCourseControllerTest extends WebTestCase
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
;
$em->remove($participation);
$em->flush();
if (NULL !== $participation) {
$em->remove($participation);
$em->flush();
}
}
public function dataGenerateRandomAccompanyingCourse()
@@ -139,9 +179,9 @@ class AccompanyingCourseControllerTest extends WebTestCase
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
// Ensure to keep always $maxGenerated to the double of $maxResults
$maxGenerated = 1;
$maxResults = 15 * 8;
// Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :)
$maxGenerated = 3;
$maxResults = $maxGenerated * 8;
static::bootKernel();
$em = static::$container->get(EntityManagerInterface::class);

View File

@@ -27,7 +27,6 @@ use Chill\PersonBundle\Entity\Person;
class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
{
public function testClosingIsAfterOpeningConsistency()
{
$datetime1 = new \DateTime('now');
@@ -77,22 +76,32 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($period->isOpen());
}
public function testCanBeReOpened()
public function testPersonPeriod()
{
$person = new Person(\DateTime::createFromFormat('Y-m-d', '2010-01-01'));
$person->close($person->getAccompanyingPeriods()[0]
->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2010-12-31')));
$firstAccompanygingPeriod = $person->getAccompanyingPeriodsOrdered()[0];
$this->assertTrue($firstAccompanygingPeriod->canBeReOpened());
$lastAccompanyingPeriod = (new AccompanyingPeriod(\DateTime::createFromFormat('Y-m-d', '2011-01-01')))
->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2011-12-31'))
;
$person->addAccompanyingPeriod($lastAccompanyingPeriod);
$this->assertFalse($firstAccompanygingPeriod->canBeReOpened());
}
$person = new Person();
$person2 = new Person();
$person3 = new Person();
$period = new AccompanyingPeriod(new \DateTime());
$period->addPerson($person);
$period->addPerson($person2);
$period->addPerson($person3);
$this->assertEquals(3, $period->getParticipations()->count());
$this->assertTrue($period->containsPerson($person));
$this->assertFalse($period->containsPerson(new Person()));
$participation = $period->getOpenParticipationContainsPerson($person);
$participations = $period->getParticipationsContainsPerson($person);
$this->assertNotNull($participation);
$this->assertSame($person, $participation->getPerson());
$this->assertEquals(1, $participations->count());
$participationL = $period->removePerson($person);
$this->assertSame($participationL, $participation);
$this->assertTrue($participation->getEndDate() instanceof \DateTimeInterface);
$participation = $period->getOpenParticipationContainsPerson($person);
$this->assertNull($participation);
}
}

View File

@@ -27,14 +27,6 @@ services:
public: true
tags:
- { name: chill.timeline, context: 'person' }
chill.person.security.authorization.person:
class: Chill\PersonBundle\Security\Authorization\PersonVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.person.birthdate_validation:
class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator

View File

@@ -46,3 +46,9 @@ services:
$dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingCourseApiController:
arguments:
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
tags: ['controller.service_arguments']

View File

@@ -23,3 +23,8 @@ services:
arguments:
- '@Doctrine\Persistence\ManagerRegistry'
tags: [ doctrine.repository_service ]
Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository:
arguments:
- '@Doctrine\Persistence\ManagerRegistry'
tags: [ doctrine.repository_service ]

View File

@@ -0,0 +1,16 @@
services:
chill.person.security.authorization.person:
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"
tags:
- { name: security.voter }
- { name: chill.role }

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create Household and HouseholdMembers tables
*/
final class Version20210505093408 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create Household and HouseholdMembers tables';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE Household_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE HouseholdMembers_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE Household (id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE HouseholdMembers (id INT NOT NULL, person_id INT DEFAULT NULL, household_id INT DEFAULT NULL, position VARCHAR(255) DEFAULT NULL, startDate DATE NOT NULL, endDate DATE NOT NULL, comment VARCHAR(255) DEFAULT NULL, sharedHousehold BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4D1FB288217BBB47 ON HouseholdMembers (person_id)');
$this->addSql('CREATE INDEX IDX_4D1FB288E79FF843 ON HouseholdMembers (household_id)');
$this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288E79FF843');
$this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288217BBB47');
$this->addSql('DROP SEQUENCE Household_id_seq CASCADE');
$this->addSql('DROP SEQUENCE HouseholdMembers_id_seq CASCADE');
$this->addSql('DROP TABLE Household');
$this->addSql('DROP TABLE HouseholdMembers');
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add a household_to_address table
*/
final class Version20210505154316 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a household_to_addresses table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_person_household_to_addresses (household_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY(household_id, address_id))');
$this->addSql('CREATE INDEX IDX_7109483E79FF843 ON chill_person_household_to_addresses (household_id)');
$this->addSql('CREATE INDEX IDX_7109483F5B7AF75 ON chill_person_household_to_addresses (address_id)');
$this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483F5B7AF75 FOREIGN KEY (address_id) REFERENCES chill_main_address (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE chill_person_household_to_addresses');
}
}

View File

@@ -190,6 +190,7 @@ CHILL_PERSON_CREATE: Ajouter des personnes
CHILL_PERSON_STATS: Statistiques sur les personnes
CHILL_PERSON_LISTS: Liste des personnes
CHILL_PERSON_DUPLICATE: Gérer les doublons de personnes
CHILL_PERSON_ACCOMPANYING_PERIOD_SEE: Voir les périodes d'accompagnement
#period
Period closed!: Période clôturée!