Merge remote-tracking branch 'origin/master' into features/activity-form

This commit is contained in:
2021-06-08 17:24:47 +02:00
130 changed files with 4288 additions and 1221 deletions

View File

@@ -390,13 +390,13 @@ class AccompanyingPeriodController extends AbstractController
/** @var Person $person */
$person = $this->_getPerson($person_id);
$criteria = Criteria::create();
$criteria->where($criteria->expr()->eq('id', $period_id));
/* @var $period AccompanyingPeriod */
$period = $person->getAccompanyingPeriods()
->matching($criteria)
->first();
$period = \array_filter(
$person->getAccompanyingPeriods(),
function (AccompanyingPeriod $p) use ($period_id) {
return $p->getId() === ($period_id);
}
)[0] ?? NULL;
if ($period === NULL) {
throw $this->createNotFoundException('period not found');

View File

@@ -0,0 +1,88 @@
<?php
namespace Chill\PersonBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Chill\PersonBundle\Entity\Household\Household;
/**
* @Route("/{_locale}/person/household")
*/
class HouseholdController extends AbstractController
{
/**
* @Route(
* "/{household_id}/summary",
* name="chill_person_household_summary",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function summary(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/summary.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/members",
* name="chill_person_household_members",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function members(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/members.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/addresses",
* name="chill_person_household_addresses",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addresses(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/addresses.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/address/move",
* name="chill_person_household_address_move",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addressMove(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/address_move.html.twig',
[
'household' => $household
]
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Chill\PersonBundle\Controller;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception;
use Symfony\Component\Routing\Annotation\Route;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\PersonBundle\Household\MembersEditor;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class HouseholdMemberController extends ApiController
{
/**
* @Route(
* "/api/1.0/person/household/members/move.{_format}",
* name="chill_person_household_members_move"
* )
*/
public function move(Request $request, $_format): Response
{
try {
$editor = $this->getSerializer()
->deserialize($request->getContent(), MembersEditor::class,
$_format, ['groups' => [ "read" ]]);
} catch (Exception\InvalidArgumentException | Exception\UnexpectedValueException $e) {
throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e);
}
// TODO ACL
//
// TODO validation
//
$em = $this->getDoctrine()->getManager();
foreach ($editor->getPersistable() as $el) {
$em->persist($el);
}
$em->flush();
return $this->json($editor->getHousehold(), Response::HTTP_OK, [], [
"groups" => ["read"],
]);
}
}

View File

@@ -27,32 +27,17 @@ use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Timeline\TimelineBuilder;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
/**
* Class TimelinePersonController
*
* @package Chill\PersonBundle\Controller
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class TimelinePersonController extends AbstractController
{
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
protected EventDispatcherInterface $eventDispatcher;
/**
*
* @var TimelineBuilder
*/
protected $timelineBuilder;
protected TimelineBuilder $timelineBuilder;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
protected PaginatorFactory $paginatorFactory;
/**
* TimelinePersonController constructor.
@@ -62,11 +47,13 @@ class TimelinePersonController extends AbstractController
public function __construct(
EventDispatcherInterface $eventDispatcher,
TimelineBuilder $timelineBuilder,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
AuthorizationHelper $authorizationHelper
) {
$this->eventDispatcher = $eventDispatcher;
$this->timelineBuilder = $timelineBuilder;
$this->paginatorFactory = $paginatorFactory;
$this->authorizationHelper = $authorizationHelper;
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Household\MembersEditorFactory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
class LoadHousehold extends Fixture implements DependentFixtureInterface
{
private MembersEditorFactory $editorFactory;
private EntityManagerInterface $em;
private CONST NUMBER_OF_HOUSEHOLD = 10;
public function __construct(MembersEditorFactory $editorFactory, EntityManagerInterface $em)
{
$this->editorFactory = $editorFactory;
$this->em = $em;
}
public function load(ObjectManager $manager)
{
// generate two times the participation. This will lead to
// some movement in participation (same people in two differents
// households)
$this->preparePersonIds();
$this->generateHousehold(
$manager,
\DateTimeImmutable::createFromFormat('Y-m-d', '2010-01-01')
);
$this->preparePersonIds();
$this->generateHousehold(
$manager,
\DateTimeImmutable::createFromFormat('Y-m-d', '2015-01-01')
);
$manager->flush();
}
private function generateHousehold(ObjectManager $manager, \DateTimeImmutable $startDate)
{
for ($i=0; $i < self::NUMBER_OF_HOUSEHOLD; $i++) {
$household = new Household();
$manager->persist($household);
$movement = $this->editorFactory->createEditor($household);
// load adults
$k = 0;
foreach ($this->getRandomPersons(1, 3) as $person) {
$date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W'));
$position = $this->getReference(LoadHouseholdPosition::ADULT);
$movement->addMovement($date, $person, $position, $k === 0, "self generated");
$k++;
}
// load children
foreach ($this->getRandomPersons(0, 3) as $person) {
$date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W'));
$position = $this->getReference(LoadHouseholdPosition::CHILD);
$movement->addMovement($date, $person, $position, $k === 0, "self generated");
$k++;
}
// load children out
foreach ($this->getRandomPersons(0, 2) as $person) {
$date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W'));
$position = $this->getReference(LoadHouseholdPosition::CHILD_OUT);
$movement->addMovement($date, $person, $position, $k === 0, "self generated");
$k++;
}
foreach ($movement->getPersistable() as $obj) {
$manager->persist($obj);
}
}
}
private function preparePersonIds()
{
$this->personIds = $this->em
->createQuery('SELECT p.id FROM '.Person::class.' p '.
'JOIN p.center c '.
'WHERE c.name = :center '
)
->setParameter('center', 'Center A')
->getScalarResult()
;
\shuffle($this->personIds);
}
private function getRandomPersons(int $min, int $max)
{
$nb = \random_int($min, $max);
for ($i=0; $i < $nb; $i++) {
$personId = \array_pop($this->personIds)['id'];
$persons[] = $this->em->getRepository(Person::class)
->find($personId)
;
}
return $persons ?? [];
}
public function getDependencies()
{
return [
LoadPeople::class,
LoadHouseholdPosition::class
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\PersonBundle\Entity\Household\Position;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class LoadHouseholdPosition extends Fixture
{
const POSITIONS_DATA = [
["Adulte", true, true, 1.0, self::ADULT ],
["Enfants", true, false, 2.0, self::CHILD ],
["Enfants hors ménage", false, false, 3.0, self::CHILD_OUT ]
];
const ADULT = "position_adulte";
const CHILD = "position_enfant";
const CHILD_OUT = "position_enfant_hors";
public function load(ObjectManager $manager)
{
foreach (self::POSITIONS_DATA as list($name, $share, $allowHolder,
$ordering, $ref)) {
$position = (new Position())
->setLabel([ "fr" => $name ])
->setAllowHolder($allowHolder)
->setShareHousehold($share)
->setOrdering($ordering)
;
$manager->persist($position);
$this->addReference($ref, $position);
}
$manager->flush();
}
}

View File

@@ -72,8 +72,8 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/command.yaml');
$loader->load('services/actions.yaml');
$loader->load('services/form.yaml');
$loader->load('services/templating.yaml');
$loader->load('services/alt_names.yaml');
$loader->load('services/household.yaml');
// We can get rid of this file when the service 'chill.person.repository.person' is no more used.
// We should use the PersonRepository service instead of a custom service name.
$loader->load('services/repository.yaml');

View File

@@ -369,13 +369,9 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return false;
}
public function setRemark(string $remark): self
public function setRemark(string $remark = null): self
{
if ($remark === null) {
$remark = '';
}
$this->remark = $remark;
$this->remark = (string) $remark;
return $this;
}
@@ -447,12 +443,13 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
*/
public function getParticipationsContainsPerson(Person $person): Collection
{
return $this->getParticipations($person)->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if ($person === $participation->getPerson()) {
return $participation;
return $this
->getParticipations()
->filter(
static function(AccompanyingPeriodParticipation $participation) use ($person): bool {
return $person === $participation->getPerson();
}
});
);
}
/**
@@ -462,12 +459,13 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
*/
public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation
{
$collection = $this->getParticipationsContainsPerson($person)->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if (NULL === $participation->getEndDate()) {
return $participation;
$collection = $this
->getParticipationsContainsPerson($person)
->filter(
static function(AccompanyingPeriodParticipation $participation): bool {
return null === $participation->getEndDate();
}
});
);
return $collection->count() > 0 ? $collection->first() : NULL;
}
@@ -557,15 +555,16 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return false;
}
$participation = $this->getParticipationsContainsPerson($person);
if (!null === $participation)
$participation = $this->getOpenParticipationContainsPerson($person);
if (null === $participation)
{
$person = $participation->getPerson();
$periods = $person->getAccompanyingPeriodsOrdered();
return end($periods) === $this;
return false;
}
return false;
$periods = $participation->getPerson()->getAccompanyingPeriodsOrdered();
return end($periods) === $this;
}
/**
@@ -863,14 +862,18 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
/**
* Get a list of all persons which are participating to this course
*
* @psalm-return Collection<int, Person>
*/
public function getPersons(): Collection
{
return $this->participations->map(
function(AccompanyingPeriodParticipation $participation) {
return $participation->getPerson();
}
);
return $this
->participations
->map(
static function(AccompanyingPeriodParticipation $participation): Person {
return $participation->getPerson();
}
);
}
public function setCreatedAt(\DateTimeInterface $datetime): self

View File

@@ -2,12 +2,21 @@
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Annotation as Serializer;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
/**
* @ORM\Entity
* @ORM\Table(
* name="chill_person_household"
* )
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household"=Household::class
* })
*/
class Household
{
@@ -15,17 +24,12 @@ class Household
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
public function getId(): ?int
{
return $this->id;
}
private ?int $id = null;
/**
* Addresses
* @var Collection
*
* @ORM\ManyToMany(
* targetEntity="Chill\MainBundle\Entity\Address",
@@ -33,8 +37,27 @@ class Household
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
*/
private $addresses;
private Collection $addresses;
/**
* @ORM\OneToMany(
* targetEntity=HouseholdMember::class,
* mappedBy="household"
* )
* @Serializer\Groups({"read"})
*/
private Collection $members;
public function __construct()
{
$this->addresses = new ArrayCollection();
$this->members = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* @param Address $address
@@ -66,4 +89,34 @@ class Household
return $this->addresses;
}
/**
* @return Collection|HouseholdMember[]
*/
public function getMembers(): Collection
{
return $this->members;
}
public function addMember(HouseholdMember $member): self
{
if (!$this->members->contains($member)) {
$this->members[] = $member;
$member->setHousehold($this);
}
return $this;
}
public function removeMember(HouseholdMember $member): self
{
if ($this->members->removeElement($member)) {
// set the owning side to null (unless already changed)
if ($member->getHousehold() === $this) {
$member->setHousehold(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity
* @ORM\Table(
* name="chill_person_household_members"
* )
*/
class HouseholdMember
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Position::class)
* @Serializer\Groups({"read"})
*/
private ?Position $position = null;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read"})
*/
private ?\DateTimeImmutable $startDate = null;
/**
* @ORM\Column(type="date_immutable", nullable= true, options={"default": null})
* @Serializer\Groups({"read"})
*/
private ?\DateTimeImmutable $endDate = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Serializer\Groups({"read"})
*/
private ?string $comment = NULL;
/**
* @ORM\Column(type="boolean")
*/
private bool $sharedHousehold = false;
/**
* @ORM\Column(type="boolean", options={"default": false})
* @Serializer\Groups({"read"})
*/
private bool $holder = false;
/**
*
* @var Person
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
* @Serializer\Groups({"read"})
*/
private ?Person $person = null;
/**
*
* @var Household
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Household\Household"
* )
*/
private ?Household $household = null;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): ?Position
{
return $this->position;
}
public function setPosition(Position $position): self
{
if ($this->position instanceof Position) {
throw new \LogicException("The position is already set. You cannot change ".
"a position of a membership");
}
$this->position = $position;
$this->sharedHousehold = $position->getShareHousehold();
return $this;
}
public function getStartDate(): ?\DateTimeImmutable
{
return $this->startDate;
}
public function setStartDate(\DateTimeImmutable $startDate): self
{
$this->startDate = $startDate;
return $this;
}
public function getEndDate(): ?\DateTimeImmutable
{
return $this->endDate;
}
public function setEndDate(\DateTimeImmutable $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;
}
/**
* @Serializer\Groups({"read"})
*/
public function getShareHousehold(): ?bool
{
return $this->sharedHousehold;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function setPerson(?Person $person): self
{
if ($this->person instanceof Person) {
throw new \LogicException("You cannot change person ".
"on a membership");
}
$this->person = $person;
$this->person->addHouseholdParticipation($this);
return $this;
}
public function getHousehold(): ?Household
{
return $this->household;
}
public function setHousehold(?Household $household): self
{
if ($this->household instanceof Household) {
throw new \LogicException("You cannot change household ".
"on a membership");
}
$this->household = $household;
return $this;
}
public function setHolder(bool $holder): self
{
$this->holder = $holder;
return $this;
}
public function isHolder(): bool
{
return $this->holder;
}
}

View File

@@ -1,152 +0,0 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
/**
* @ORM\Entity
*/
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

@@ -0,0 +1,98 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Chill\PersonBundle\Repository\Household\PositionRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity(repositoryClass=PositionRepository::class)
* @ORM\Table(name="chill_person_household_position")
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household_position"=Position::class
* })
*/
class Position
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({ "read" })
*/
private $id;
/**
* @ORM\Column(type="json")
*/
private $label = [];
/**
* @ORM\Column(type="boolean")
*/
private $shareHouseHold;
/**
* @ORM\Column(type="boolean")
*/
private $allowHolder;
/**
* @ORM\Column(type="float")
*/
private $ordering;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?array
{
return $this->label;
}
public function setLabel(array $label): self
{
$this->label = $label;
return $this;
}
public function getShareHousehold(): ?bool
{
return $this->shareHouseHold;
}
public function setShareHousehold(bool $shareHouseHold): self
{
$this->shareHouseHold = $shareHouseHold;
return $this;
}
public function getAllowHolder(): ?bool
{
return $this->allowHolder;
}
public function setAllowHolder(bool $allowHolder): self
{
$this->allowHolder = $allowHolder;
return $this;
}
public function getOrdering(): ?float
{
return $this->ordering;
}
public function setOrdering(float $ordering): self
{
$this->ordering = $ordering;
return $this;
}
}

View File

@@ -26,6 +26,7 @@ use ArrayIterator;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Country;
use Chill\PersonBundle\Entity\MaritalStatus;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\Address;
use DateTime;
@@ -272,6 +273,14 @@ class Person implements HasCenterInterface
*/
private $fullnameCanonical;
/**
* @ORM\OneToMany(
* targetEntity=HouseholdMember::class,
* mappedBy="person"
* )
*/
private Collection $householdParticipations;
/**
* Person constructor.
*
@@ -284,6 +293,7 @@ class Person implements HasCenterInterface
$this->addresses = new ArrayCollection();
$this->altNames = new ArrayCollection();
$this->otherPhoneNumbers = new ArrayCollection();
$this->householdParticipations = new ArrayCollection();
if ($opening === null) {
$opening = new \DateTime();
@@ -1180,4 +1190,16 @@ class Person implements HasCenterInterface
$this->fullnameCanonical = $fullnameCanonical;
return $this;
}
public function addHouseholdParticipation(HouseholdMember $member): self
{
$this->householdParticipations[] = $member;
return $this;
}
public function getHouseholdParticipations(): Collection
{
return $this->householdParticipations;
}
}

View File

@@ -21,31 +21,19 @@ namespace Chill\PersonBundle\Form\ChoiceLoader;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Doctrine\ORM\EntityRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
/**
* Class PersonChoiceLoader
*
* @package Chill\PersonBundle\Form\ChoiceLoader
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* Allow to load a list of person
*/
class PersonChoiceLoader implements ChoiceLoaderInterface
{
/**
* @var EntityRepository
*/
protected $personRepository;
protected PersonRepository $personRepository;
/**
* @var array
*/
protected $lazyLoadedPersons = [];
protected array $lazyLoadedPersons = [];
/**
* @var array
*/
protected $centers = [];
protected array $centers = [];
/**
* PersonChoiceLoader constructor.
@@ -54,7 +42,7 @@ class PersonChoiceLoader implements ChoiceLoaderInterface
* @param array|null $centers
*/
public function __construct(
EntityRepository $personRepository,
PersonRepository $personRepository,
array $centers = null
) {
$this->personRepository = $personRepository;

View File

@@ -0,0 +1,79 @@
<?php
namespace Chill\PersonBundle\Household;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class MembersEditor
{
private ValidatorInterface $validator;
private Household $household;
private array $persistables = [];
private array $membershipsAffected = [];
public function __construct(ValidatorInterface $validator, Household $household)
{
$this->validation = $validator;
$this->household = $household;
}
public function addMovement(\DateTimeImmutable $date, Person $person, Position $position, ?bool $holder = false, ?string $comment = null): self
{
if (NULL === $this->household) {
throw new \LogicException("You must define a household first");
}
$membership = (new HouseholdMember())
->setStartDate($date)
->setPerson($person)
->setPosition($position)
->setHolder($holder)
->setHousehold($this->household)
->setComment($comment)
;
if ($position->getShareHousehold()) {
foreach ($person->getHouseholdParticipations() as $participation) {
if (FALSE === $participation->getShareHousehold()) {
continue;
}
if ($participation === $membership) {
continue;
}
if ($participation->getEndDate() === NULL || $participation->getEndDate() > $date) {
$participation->setEndDate($date);
$this->membershipsAffected[] = $participation;
}
}
}
$this->membershipsAffected[] = $membership;
$this->persistables[] = $membership;
return $this;
}
public function validate(): ConstraintViolationListInterface
{
}
public function getPersistable(): array
{
return $this->persistables;
}
public function getHousehold(): Household
{
return $this->household;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Chill\PersonBundle\Household;
use Chill\PersonBundle\Household\MembersEditor;
use Chill\PersonBundle\Entity\Household\Household;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class MembersEditorFactory
{
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function createEditor(Household $household): MembersEditor
{
return new MembersEditor($this->validator, $household);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Knp\Menu\MenuItem;
use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public static function getMenuIds(): array
{
return [ 'household' ];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
$household = $parameters['household'];
$menu->addChild($this->translator->trans('Summary'), [
'route' => 'chill_person_household_summary',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 10]);
$menu->addChild($this->translator->trans('Members'), [
'route' => 'chill_person_household_members',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 20]);
$menu->addChild($this->translator->trans('Addresses'), [
'route' => 'chill_person_household_addresses',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 30]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Position|null find($id, $lockMode = null, $lockVersion = null)
* @method Position|null findOneBy(array $criteria, array $orderBy = null)
* @method Position[] findAll()
* @method Position[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PositionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Position::class);
}
}

View File

@@ -9,8 +9,7 @@
<table class="rounded" v-if="participations.length > 0">
<thead>
<tr>
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th>
<th class="chill-orange">{{ $t('persons_associated.lastname') }}</th>
<th class="chill-orange">{{ $t('persons_associated.name') }}</th>
<th class="chill-orange">{{ $t('persons_associated.startdate') }}</th>
<th class="chill-orange">{{ $t('persons_associated.enddate') }}</th>
<th class="chill-orange">{{ $t('action.actions') }}</th>

View File

@@ -1,7 +1,9 @@
<template>
<tr>
<td>{{ participation.person.firstName }}</td>
<td>{{ participation.person.lastName }}</td>
<td>
{{ participation.person.firstName }}
{{ participation.person.lastName }}
</td>
<td><span v-if="participation.startDate">
{{ $d(participation.startDate.datetime, 'short') }}</span>
</td>
@@ -11,16 +13,18 @@
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="show">
</on-the-fly>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="edit">
</on-the-fly>
</li>
<!--li>
<button class="sc-button bt-delete"
@@ -31,7 +35,7 @@
<li>
<button v-if="!participation.endDate"
class="sc-button bt-remove"
:title="$t('action.remove')"
v-bind:title="$t('action.remove')"
@click.prevent="$emit('close', participation)">
</button>
<button v-else class="sc-button bt-remove disabled"></button>
@@ -42,17 +46,14 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'PersonItem',
props: ['participation'],
data() {
return {
url: {
show: '/fr/person/' + this.participation.person.id + '/general',
edit: '/fr/person/' + this.participation.person.id + '/general/edit'
}
}
components: {
OnTheFly
},
emits: ['remove', 'close']
props: ['participation'],
emits: ['remove', 'close'],
}
</script>

View File

@@ -11,44 +11,50 @@
</label>
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<h4>
{{ accompanyingCourse.requestor.text }}
</h4>
<span class="badge badge-pill badge-secondary">{{ accompanyingCourse.requestor.type }}</span>
</div>
<div class="item-col">
<ul class="content-bloc" v-if="accompanyingCourse.requestor.type === 'person'">
<li><i>{{ $t('requestor.birthdate') }}</i>
{{ $d(accompanyingCourse.requestor.birthdate.datetime, 'short') }}
</li>
<li><i>{{ $t('requestor.center') }}</i>
{{ accompanyingCourse.requestor.center.name }}
</li>
<li><i>{{ $t('requestor.phonenumber') }}</i>
{{ accompanyingCourse.requestor.phonenumber }}
</li>
<li><i>{{ $t('requestor.mobilenumber') }}</i>
{{ accompanyingCourse.requestor.mobilenumber }}
</li>
</ul>
<ul class="content-bloc" v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<dt>{{ $t('requestor.address') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.text }}</dd>
<dt>{{ $t('requestor.location') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.postcode.name }}</dd>
</ul>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" :title="$t('action.show')" target="_blank" :href="url.show"></a>
</li>
</ul>
</div>
</div>
<h4>
<span class="badge badge-pill badge-secondary">{{ accompanyingCourse.requestor.type }}</span>
{{ accompanyingCourse.requestor.text }}
</h4>
<dl class="content-bloc" v-if="accompanyingCourse.requestor.type === 'person'">
<dt>{{ $t('requestor.birthdate') }}</dt>
<dd>{{ $d(accompanyingCourse.requestor.birthdate.datetime, 'short') }}</dd>
<dt>{{ $t('requestor.center') }}</dt>
<dd>{{ accompanyingCourse.requestor.center.name }}</dd>
<dt>{{ $t('requestor.phonenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.phonenumber }}</dd>
<dt>{{ $t('requestor.mobilenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.mobilenumber }}</dd>
</dl>
<dl class="content-bloc" v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<dt>{{ $t('requestor.address') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.text }}</dd>
<dt>{{ $t('requestor.location') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.postcode.name }}</dd>
</dl>
<ul class="record_actions">
<li>
<on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="show">
</on-the-fly>
</li>
<li>
<on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="edit">
</on-the-fly>
</li>
</ul>
</div>
<ul class="record_actions">
<li>
@@ -79,12 +85,14 @@
</template>
<script>
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'Requestor',
components: {
AddPersons,
OnTheFly
},
data() {
return {
@@ -110,13 +118,6 @@ export default {
get() {
return this.$store.state.accompanyingCourse.requestorAnonymous;
}
},
url() {
return (this.accompanyingCourse.requestor.type === 'person') ? {
show: `/fr/person/${this.accompanyingCourse.requestor.id}/general`,
} : {
show: `/fr/thirdparty/thirdparty/${this.accompanyingCourse.requestor.id}/show`,
}
}
},
methods: {

View File

@@ -21,24 +21,25 @@
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
<on-the-fly
v-bind:type="resource.resource.type"
v-bind:id="resource.resource.id"
action="show">
</on-the-fly>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
<on-the-fly
v-bind:type="resource.resource.type"
v-bind:id="resource.resource.id"
action="edit">
</on-the-fly>
</li>
<li>
<button
class="sc-button bt-remove"
:title="$t('action.remove')"
v-bind:title="$t('action.remove')"
@click.prevent="$emit('remove', resource)">
</button>
</li>
</ul>
</td>
@@ -46,23 +47,14 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'ResourceItem',
components: {
OnTheFly
},
props: ['resource'],
emits: ['remove'],
computed: {
type() {
return this.resource.resource.type;
},
url() {
return (this.type === 'person') ? {
show: `/fr/person/${this.resource.resource.id}/general`,
edit: `/fr/person/${this.resource.resource.id}/general/edit`
} : {
show: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/show`,
edit: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/update`
}
}
}
emits: ['remove']
}
</script>

View File

@@ -31,6 +31,7 @@ const appMessages = {
counter: "Il n'y a pas encore d'usager | 1 usager | {count} usagers",
firstname: "Prénom",
lastname: "Nom",
name: "Nom",
startdate: "Date d'entrée",
enddate: "Date de sortie",
add_persons: "Ajouter des usagers",
@@ -73,7 +74,7 @@ const appMessages = {
comment: {
title: "Observations",
label: "Ajout d'une note",
content: "Rédigez une première note...",
content: "Rédigez une première note",
created_by: "créé par {0}, le {1}"
},
confirm: {

View File

@@ -0,0 +1,34 @@
/*
* GET a person by id
*/
const getPerson = (id) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* POST a new person
*/
const postPerson = (body) => {
const url = `/api/1.0/person/person.json`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
getPerson,
postPerson
};

View File

@@ -65,9 +65,14 @@
@updateSelected="updateSelected">
</person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
{{ $t('action.create') }} "{{ query }}"
</button>
<div class="create-button">
<on-the-fly
v-if="query.length >= 3"
v-bind:buttonText="$t('onthefly.create.button', {q: query})"
action="create"><!-- TODO first close this modal -->
</on-the-fly>
</div>
</div>
</template>
@@ -84,6 +89,7 @@
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
@@ -92,6 +98,7 @@ export default {
components: {
Modal,
PersonSuggestion,
OnTheFly
},
props: [
'buttonTitle',
@@ -170,7 +177,7 @@ export default {
if (query.length >= 3) {
searchPersons_2({ query, options: this.options })
.then(suggested => new Promise((resolve, reject) => {
console.log('suggested', suggested);
//console.log('suggested', suggested);
this.loadSuggestions(suggested.results);
resolve();
}));
@@ -179,14 +186,14 @@ export default {
}
},
loadSuggestions(suggested) {
console.log('suggested', suggested);
//console.log('suggested', suggested);
this.search.suggested = suggested;
this.search.suggested.forEach(function(item) {
item.key = this.itemKey(item);
}, this);
},
updateSelected(value) {
console.log('value', value);
//console.log('value', value);
this.search.selected = value;
},
resetSearch() {
@@ -256,4 +263,8 @@ export default {
}
}
}
.create-button > a {
margin-top: 0.5em;
margin-left: 2.6em;
}
</style>

View File

@@ -42,7 +42,7 @@ export default {
computed: {
selected: {
set(value) {
console.log('value', value);
//console.log('value', value);
this.$emit('updateSelected', value);
},
get() {

View File

@@ -14,22 +14,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_person') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
<on-the-fly
type="person"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'SuggestionPerson',
props: ['item'],
data() {
return {
url: {
show: '/fr/person/' + this.item.result.person_id + '/general',
edit: '/fr/person/' + this.item.result.person_id + '/general/edit'
},
}
components: {
OnTheFly
},
props: ['item']
}
</script>

View File

@@ -15,22 +15,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'SuggestionThirdParty',
props: ['item'],
data() {
return {
url: {
show: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/show',
edit: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/edit'
},
}
components: {
OnTheFly
},
props: ['item']
}
</script>

View File

@@ -0,0 +1,200 @@
<template>
<div v-if="action === 'show'">
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<h3 :title="person.id">{{ person.text }}</h3>
<p>
<i class="fa fa-fw"
:class="genderClass">
<!--
:title="$t(genderTranslation)"
-->
</i>
<span v-if="person.birthdate">
{{ $t('person.born', { e: feminized }) }}
{{ $d(person.birthdate.datetime, 'short') }}
</span>
</p>
</div>
<div class="item-col">
<dl class="list-content">
<dt>{{ $t('person.firstname') }}</dt>
<dd>{{ person.firstName }}</dd>
<dt>{{ $t('person.lastname') }}</dt>
<dd>{{ person.lastName }}</dd>
<dt>{{ $t('person.altnames') }}</dt>
<dd>{{ person.altNames }}</dd>
<span v-if="person.center">
<dt>{{ $t('person.center_name') }}</dt>
<dd :title="person.center.id">{{ person.center.name }}</dd>
</span>
<dt>{{ $t('person.phonenumber') }}</dt>
<dd>{{ person.phonenumber }}</dd>
<dt>{{ $t('person.mobilenumber') }}</dt>
<dd>{{ person.mobilenumber }}</dd>
<dt>{{ $t('person.gender.title') }}</dt>
<!--
<dd>{{ $t(genderTranslation) }}</dd>
-->
</dl>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<input v-model="firstName" :placeholder="$t('person.firstname')" />
<input v-model="lastName" :placeholder="$t('person.lastname')" />
<!-- TODO fix placeholder if undefined
TODO dynamically get gender options
-->
<select v-model="gender">
<option disabled value="">{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option>
<option value="neuter">{{ $t('person.gender.neuter') }}</option>
</select>
<i class="fa fa-birthday-cake"></i>
<input type="date"
id="chill_personbundle_person_birthdate"
name="chill_personbundle_person[birthdate]"
v-model="birthDate"
/>
<i class="fa fa-phone">
</i><input v-model="phonenumber" :placeholder="$t('person.phonenumber')" />
<i class="fa fa-mobile">
</i><input v-model="mobilenumber" :placeholder="$t('person.mobilenumber')" />
</div>
</template>
<script>
import { getPerson, postPerson } from '../../_api/OnTheFly';
export default {
name: "OnTheFlyPerson",
props: ['id', 'type', 'action'],
data() {
return {
person: {
type: 'person'
}
}
},
computed: {
firstName: {
set(value) { this.person.firstName = value; },
get() { return this.person.firstName; }
},
lastName: {
set(value) { this.person.lastName = value; },
get() { return this.person.lastName; }
},
gender: {
set(value) { this.person.gender = value; },
get() { return this.person.gender; }
},
birthDate: {
set(value) {
if (this.person.birthdate) {
this.person.birthdate.datetime = value + "T00:00:00+0100";
} else {
this.person.birthdate = { datetime: value + "T00:00:00+0100"};
}
},
get() {
return (this.person.birthdate) ? this.person.birthdate.datetime.split('T')[0] : '';
}
},
phonenumber: {
set(value) { this.person.phonenumber = value; },
get() { return this.person.phonenumber; }
},
mobilenumber: {
set(value) { this.person.mobilenumber = value; },
get() { return this.person.mobilenumber; }
},
genderClass() {
switch (this.person.gender) {
case 'woman':
return 'fa-venus';
case 'man':
return 'fa-mars';
case 'neuter':
return 'fa-neuter';
}
},
genderTranslation() {
switch (this.person.gender) {
case 'woman':
return 'person.gender.woman';
case 'man':
return 'person.gender.man';
case 'neuter':
return 'person.gender.neuter';
}
},
feminized() {
return (this.person.gender === 'woman')? 'e' : '';
}
},
mounted() {
if (this.action !== 'create') {
this.loadData();
}
},
methods: {
loadData() {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('get person', this.person);
resolve();
}));
},
postData() {
postPerson(this.person)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('post person', person);
resolve();
}));
}
}
}
</script>
<style lang="scss" scoped>
ul {
li::marker {
}
}
div.flex-table {
div.item-bloc {
div.item-row {
div.item-col:last-child {
justify-content: flex-start;
}
}
}
}
dl {
dd {
margin-left: 1em;
}
}
</style>

View File

@@ -15,7 +15,21 @@ const personMessages = {
person: {
firstname: "Prénom",
lastname: "Nom",
born: "né le ",
born: "né{e} le ",
center_id: "Identifiant du centre",
center_type: "Type de centre",
center_name: "Territoire", // vendée
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altnames: "Autres noms",
gender: {
title: "Genre",
placeholder: "Choisissez le genre de l'usager",
woman: "Femme",
man: "Homme",
neuter: "Neutre",
}
},
error_only_one_person: "Une seule personne peut être sélectionnée !"
}

View File

@@ -31,7 +31,7 @@
{% set gender = (p.person.gender == 'woman') ? 'fa-venus' :
(p.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (p.person.gender == 'woman') ? 'femme' :
(p.person.gender == 'homme') ? 'fa-mars' : 'neutre' %}
(p.person.gender == 'man') ? 'homme' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ p.person.birthdate|format_date('short') }}
</p>
</div>

View File

@@ -0,0 +1,11 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Move household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Addresses history for household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<a class="sc-button bt-update"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
{% endblock %}

View File

@@ -0,0 +1,26 @@
<div class="subheader">
<div class="grid-12 parent" id="header-accompanying_course-name" >
<div class="grid-10 push-1 grid-mobile-12 grid-tablet-12 push-mobile-0 push-tablet-0 parent">
<div class="grid-6">{% set title = title %}
<h1>
<i class="fa fa-child"></i>
{{ 'Household'|trans }}
<span style="font-weight: lighter; font-size: 50%;">(n°{{ household.id }})</span>
</h1>
</div>
<div class="grid-3" id="banner-flags"></div>
<div class="grid-3" id="banner-status"></div>
</div>
</div>
<div class="grid-12 parent" id="header-accompanying_course-details" >
<div class="grid-10 push-1 grid-mobile-12 grid-tablet-12 push-mobile-0 push-tablet-0 parent">
<div id="banner-misc"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
{% extends "ChillMainBundle::layoutWithVerticalMenu.html.twig" %}
{% block top_banner %}
{{ include('@ChillPerson/Household/banner.html.twig', { title: block('title') }) }}
{% endblock %}
{% block layout_wvm_content %}
{% block content %}{% endblock %}
{% endblock %}
{% block vertical_menu_content %}
{{ chill_menu('household', {
'layout': '@ChillPerson/Household/menu.html.twig',
'args' : { 'household': household }
}) }}
{% endblock %}

View File

@@ -0,0 +1,10 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Household members'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
{% endblock %}

View File

@@ -0,0 +1,7 @@
<ul class="tab-nav">
{% for menu in menus %}
<li class="">
<a href="{{ menu.uri }}" >{{ menu.label|upper }}</a>
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,14 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Household summary'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<h2>{{ 'Actual household members'|trans }}</h2>
<p>TODO</p>
{% endblock %}

View File

@@ -1,11 +1,24 @@
<h3 class="single-line">
<div>
<h3 class="single-line">
{{ period.closingDate|format_date('long') }}
<span class="person"> /
<a href="{{ path('chill_person_accompanying_period_list', { 'person_id': person.id } ) }}">
{{ 'Closing the accompanying period' | trans }}
</a>
<span>
<span class="chill-red">
<i class="fa fa-folder"></i>
</span>
</h3>
<span class="person"> / </span>
{{ 'An accompanying period ends'|trans }}
{% if 'person' != context %}
{% for p in period.persons %}
/ {{ p|chill_entity_render_box({'addLink': true}) }}
{% endfor %}
{% endif %}
</h3>
<div class="statement">
<dl class="chill_view_data">
<dd>{{ 'Participants'|trans }}&nbsp;:</dd>
<dt>
<ul>
{% for p in period.participations %}
<li>{{ p.person|chill_entity_render_box({ 'addLink': true }) }}: {{ 'since %date%'|trans({'%date%': p.startDate|format_date("long") } ) }}, {{ 'until %date%'|trans({'%date%': (p.endDate is not null ? p.endDate : period.closingDate)|format_date("long") }) }}</li>
{% endfor %}
</ul>
</dt>
</div>
</div>

View File

@@ -1,11 +1,24 @@
<h3 class="single-line">
<div>
<h3 class="single-line">
{{ period.openingDate|format_date('long') }}
<span class="person"> /
<a href="{{ path('chill_person_accompanying_period_list', { 'person_id': person.id } ) }}">
{{ 'Opening the accompanying period' | trans }}
</a>
</span>
<span class="chill-green">
<i class="fa fa-folder-open"></i>
</span>
</h3>
<span class="person"> / </span>
{{ 'An accompanying period starts'|trans }}
{% if 'person' != context %}
{% for p in period.persons %}
/ {{ p|chill_entity_render_box({'addLink': true}) }}
{% endfor %}
{% endif %}
</h3>
<div class="statement">
<dl class="chill_view_data">
<dd>{{ 'Participants'|trans }}&nbsp;:</dd>
<dt>
<ul>
{% for p in period.participations %}
<li>{{ 'Since %date%'|trans( {'%date%': p.startDate|format_date("long") } ) }}&nbsp;: {{ p.person|chill_entity_render_box({ 'addLink': true }) }}</li>
{% endfor %}
</ul>
</dt>
</div>
</div>

View File

@@ -0,0 +1,75 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception;
use Chill\PersonBundle\Household\MembersEditorFactory;
use Chill\PersonBundle\Household\MembersEditor;
class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
private MembersEditorFactory $factory;
use DenormalizerAwareTrait;
public function __construct(MembersEditorFactory $factory)
{
$this->factory = $factory;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$household = $this->denormalizer->denormalize($data['destination'], Household::class,
$format, $context);
if (NULL === $household) {
throw new Exception\InvalidArgumentException("household could not be denormalized. Impossible to process");
}
$editor = $this->factory->createEditor($household);
if (NULL == $data['concerned'] ?? []
&& FALSE === ·\is_array('concerned')) {
throw new Exception\UnexpectedValueException("The schema does not have any key 'concerned'");
}
foreach ($data['concerned'] as $key => $concerned) {
$person = $this->denormalizer->denormalize($concerned['person'] ?? null, Person::class,
$format, $context);
$position = $this->denormalizer->denormalize($concerned['position'] ?? null, Position::class,
$format, $context);
$startDate = $this->denormalizer->denormalize($concerned['start_date'] ?? null, \DateTimeImmutable::class,
$format, $context);
$holder = (bool) $concerned['holder'] ?? false;
$comment = (string) $concerned['comment'] ?? false;
if (
NULL === $person
&& NULL === $position
&& NULL === $startDate
) {
throw new Exception\InvalidArgumentException("position with ".
"key $key could not be denormalized: missing ".
"person, position or start_date.");
}
$editor->addMovement($startDate, $person, $position, $holder,
$comment);
return $editor;
}
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return $type === MembersEditor::class;
}
}

View File

@@ -14,14 +14,10 @@ class SocialIssueNormalizer implements NormalizerInterface, NormalizerAwareInter
use NormalizerAwareTrait;
/**
* @param SocialIssueRender $render
*/
public function __construct(SocialIssueRender $render)
{
$this->render = $render;
}
public function normalize($socialIssue, string $format = null, array $context = [])
{

View File

@@ -23,6 +23,8 @@ namespace Chill\PersonBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\AbstractChillEntityRender;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Symfony\Component\Templating\EngineInterface;
/**
* Render a Person
@@ -30,15 +32,16 @@ use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
*/
class PersonRender extends AbstractChillEntityRender
{
/**
*
* @var ConfigPersonAltNamesHelper
*/
protected $configAltNamesHelper;
private ConfigPersonAltNamesHelper $configAltNamesHelper;
private EngineInterface $engine;
public function __construct(ConfigPersonAltNamesHelper $configAltNamesHelper)
{
public function __construct(
ConfigPersonAltNamesHelper $configAltNamesHelper,
EngineInterface $engine
) {
$this->configAltNamesHelper = $configAltNamesHelper;
$this->engine = $engine;
}
/**
@@ -49,13 +52,13 @@ class PersonRender extends AbstractChillEntityRender
*/
public function renderBox($person, array $options): string
{
return
$this->getDefaultOpeningBox('person').
'<span class="chill_denomination">'.$person->getFirstName().'</span>'.
' <span class="chill_denomination">'.$person->getLastName().'</span>'.
$this->addAltNames($person, true).
$this->getDefaultClosingBox()
;
return $this->engine->render('@ChillPerson/Entity/person.html.twig',
[
'person' => $person,
'addAltNames' => $this->configAltNamesHelper->hasAltNames(),
'addLink' => $options['addLink'] ?? false
]
);
}
/**
@@ -69,7 +72,7 @@ class PersonRender extends AbstractChillEntityRender
return $person->getFirstName().' '.$person->getLastName()
.$this->addAltNames($person, false);
}
protected function addAltNames(Person $person, bool $addSpan)
{
$str = '';

View File

@@ -5,9 +5,10 @@ namespace Chill\PersonBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use RuntimeException;
use Symfony\Component\Templating\EngineInterface;
class SocialIssueRender implements ChillEntityRenderInterface
final class SocialIssueRender implements ChillEntityRenderInterface
{
private TranslatableStringHelper $translatableStringHelper;
private EngineInterface $engine;
@@ -28,11 +29,14 @@ class SocialIssueRender implements ChillEntityRenderInterface
{
return $entity instanceof SocialIssue;
}
/**
* @param SocialIssue $socialIssue
*/
public function renderString($socialIssue, array $options): string
{
/** @var $socialIssue SocialIssue */
$options = \array_merge(self::DEFAULT_ARGS, $options);
$options = array_merge(self::DEFAULT_ARGS, $options);
$str = $this->translatableStringHelper->localize($socialIssue->getTitle());
@@ -46,26 +50,36 @@ class SocialIssueRender implements ChillEntityRenderInterface
return $str;
}
protected function buildParents($socialIssue): array
protected function buildParents(SocialIssue $socialIssue): array
{
$parents = [];
while ($socialIssue->hasParent()) {
$socialIssue = $parents[] = $socialIssue->getParent();
}
return $parents;
}
}
/**
*
* @param SocialIssue $socialIssue
*/
public function renderBox($socialIssue, array $options): string
{
$options = \array_merge(self::DEFAULT_ARGS, $options);
$options = array_merge(self::DEFAULT_ARGS, $options);
// give some help to twig: an array of parents
$parents = $this->buildParents($socialIssue);
return $this->engine->render('@ChillPerson/Entity/social_issue.html.twig', [
'socialIssue' => $socialIssue,
'parents' => $parents,
'options' => $options
]);
return $this
->engine
->render(
'@ChillPerson/Entity/social_issue.html.twig',
[
'socialIssue' => $socialIssue,
'parents' => $parents,
'options' => $options
]
);
}
}

View File

@@ -28,7 +28,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
$this->assertResponseRedirects();
$location = $this->client->getResponse()->headers->get('Location');
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location));
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/edit$|", $location));
}
@@ -48,7 +48,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
$location = $this->client->getResponse()->headers->get('Location');
$matches = [];
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location, $matches));
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/edit$|", $location, $matches));
$id = $matches[1];
$period = self::$container->get(EntityManagerInterface::class)

View File

@@ -148,7 +148,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase
* Test the closing of a periods
*
* Given that a person as an accompanying period opened since 2015-01-05
* and we fill the close form (at /en/person/[id]/accompanying-period/close
* and we fill the close form (at /fr/person/[id]/accompanying-period/close
* with : dateClosing: 2015-02-01
* with : the last closing motive in list
* Then the response should redirect to period view
@@ -158,10 +158,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testClosingCurrentPeriod()
{
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/close');
$form = $crawler->selectButton('Close accompanying period')->form();
$form = $crawler->selectButton('Clôre la période')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
@@ -171,7 +171,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase
$cr = $this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isRedirect(
'/en/person/'.$this->person->getId().'/accompanying-period'),
'/fr/person/'.$this->person->getId().'/accompanying-period'),
'the server redirects to /accompanying-period page');
$this->assertGreaterThan(0, $this->client->followRedirect()
->filter('.success')->count(),
@@ -182,7 +182,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase
* Test the closing of a periods
*
* Given that a person as an accompanying period opened since 2015-01-05
* and we fill the close form (at /en/person/[id]/accompanying-period/close
* and we fill the close form (at /fr/person/[id]/accompanying-period/close
* with : dateClosing: 2014-01-01
* with : the last closing motive in list
* Then the response should redirect to period view
@@ -192,10 +192,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testClosingCurrentPeriodWithDateClosingBeforeOpeningFails()
{
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/close');
$form = $crawler->selectButton('Close accompanying period')->form();
$form = $crawler->selectButton('Clôre la période')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
@@ -223,10 +223,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testAddNewPeriodBeforeActual()
{
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton('Créer une période d\'accompagnement')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -237,7 +237,7 @@ class AccompanyingPeriodControllerTest extends WebTestCase
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isRedirect(
'/en/person/'.$this->person->getId().'/accompanying-period'),
'/fr/person/'.$this->person->getId().'/accompanying-period'),
'the server redirects to /accompanying-period page');
$this->assertGreaterThan(0, $this->client->followRedirect()
->filter('.success')->count(),
@@ -257,10 +257,13 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testCreatePeriodWithClosingAfterCurrentFails()
{
$crawler = $this->client->request('GET', '/en/person/'
$this->markTestSkipped("Multiple period may now cover. This test is kept ".
"in case of a configuration may add this feature again");
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton("Créer une période d'accompagnement")->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -289,10 +292,13 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testCreatePeriodWithOpeningAndClosingAfterCurrentFails()
{
$crawler = $this->client->request('GET', '/en/person/'
$this->markTestSkipped("Multiple period may now cover. This test is kept ".
"in case of a configuration may add this feature again");
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton("Créer une période d'accompagnement")->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -330,10 +336,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
]
));
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();;
$form = $crawler->selectButton('Créer une période d\'accompagnement')->form();;
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -361,10 +367,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
*/
public function testCreatePeriodWithClosingBeforeOpeningFails()
{
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton('Créer une période d\'accompagnement')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -403,10 +409,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
]
));
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton('Créer une période d\'accompagnement')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -444,10 +450,10 @@ class AccompanyingPeriodControllerTest extends WebTestCase
]
));
$crawler = $this->client->request('GET', '/en/person/'
$crawler = $this->client->request('GET', '/fr/person/'
.$this->person->getId().'/accompanying-period/create');
$form = $crawler->selectButton('Create an accompanying period')->form();
$form = $crawler->selectButton('Créer une période d\'accompagnement')->form();
$form->get(self::CLOSING_MOTIVE_INPUT)
->setValue($this->getLastValueOnClosingMotive($form));
$form->get(self::CLOSING_INPUT)
@@ -498,7 +504,8 @@ class AccompanyingPeriodControllerTest extends WebTestCase
//$criteria->where(Criteria::expr()->eq('openingDate', \DateTime::createFromFormat()))
$firstPeriod = reset($periods);
$lastPeriod = end($periods);
$this->markTestSkipped("From here, the test should be rewritten");
// test that it is not possible to open the first period in the list
$this->client->request('GET',
sprintf('/fr/person/%d/accompanying-period/%d/re-open', $this->person->getId(), reset($periods)->getId())
@@ -523,4 +530,4 @@ class AccompanyingPeriodControllerTest extends WebTestCase
"Test the response is a redirection => the period is re-opened");
}
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class AdminControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/{_locale}/admin/person');
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Chill\PersonBundle\Entity\Household\Household;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
class HouseholdControllerTest extends WebTestCase
{
use PrepareClientTrait;
private ?KernelBrowser $client = null;
protected function setUp()
{
$this->client = $this->getClientAuthenticated();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testSummary($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/summary"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testMembers($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/members"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testAddresses($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/addresses"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testAddressMove($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/address/move"
);
$this->assertResponseIsSuccessful();
// ici, il faudrait tester la requête POST
}
public function generateValidHouseholdIds()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$ids = $em->createQuery("SELECT DISTINCT h.id FROM ".Household::class." h ".
"JOIN h.members m ".
"JOIN m.person p ".
"JOIN p.center c ".
"WHERE c.name = :center"
)
->setParameter('center', "Center A")
->setMaxResults(100)
->getScalarResult()
;
\shuffle($ids);
yield [ \array_pop($ids)['id'] ];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Bundle\ChillPersonBundle\Tests\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Doctrine\ORM\EntityManagerInterface;
class HouseholdMemberControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider provideValidData
*/
public function testMoveMember($personId, $householdId, $positionId, \DateTimeInterface $date)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_POST,
'/api/1.0/person/household/members/move.json',
[], // parameters
[], // files
[], // server
\json_encode(
[
'concerned' =>
[
[
'person' =>
[
'type' => 'person',
'id' => $personId
],
'start_date' =>
[
'datetime' => $date->format(\DateTimeInterface::RFC3339)
],
'position' =>
[
'type' => 'household_position',
'id' => $positionId
],
'holder' => false,
'comment' => "Introduced by automated test",
],
],
'destination' =>
[
'type' => 'household',
'id' => $householdId
]
],
true)
);
$this->assertEquals(Response::HTTP_OK,
$client->getResponse()->getStatusCode()
);
}
public function provideValidData(): \Iterator
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$personIds = $em->createQuery("SELECT p.id FROM ".Person::class." p ".
"JOIN p.center c WHERE c.name = :center")
->setParameter('center', "Center A")
->setMaxResults(100)
->getScalarResult()
;
\shuffle($personIds);
$household = new Household();
$em->persist($household);
$em->flush();
$positions = $em->createQuery("SELECT pos.id FROM ".Position::class." pos ".
"WHERE pos.shareHouseHold = TRUE")
->getResult()
;
yield [
\array_pop($personIds)['id'],
$household->getId(),
$positions[\random_int(0, count($positions) - 1)]['id'],
new \DateTimeImmutable('today')
];
}
}

View File

@@ -25,7 +25,7 @@ namespace Chill\PersonBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\DomCrawler\Form;
use Chill\MainBundle\Test\PrepareClientTrait;
use \Symfony\Component\BrowserKit\Client;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
/**
* Test creation and deletion for persons
@@ -34,7 +34,7 @@ class PersonControllerCreateTest extends WebTestCase
{
use PrepareClientTrait;
private Client $client;
private KernelBrowser $client;
const FIRSTNAME_INPUT = 'chill_personbundle_person_creation[firstName]';
const LASTNAME_INPUT = "chill_personbundle_person_creation[lastName]";
@@ -59,8 +59,8 @@ class PersonControllerCreateTest extends WebTestCase
string $firstname = 'God',
string $lastname = 'Jesus'
) {
$creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname);
$creationForm->get(self::LASTNAME_INPUT)->setValue($lastname);
$creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname.'_'.uniqid());
$creationForm->get(self::LASTNAME_INPUT)->setValue($lastname.'_'.uniqid());
$creationForm->get(self::GENDER_INPUT)->select("man");
$date = new \DateTime('1947-02-01');
$creationForm->get(self::BIRTHDATE_INPUT)->setValue($date->format('d-m-Y'));
@@ -114,20 +114,6 @@ class PersonControllerCreateTest extends WebTestCase
return $form;
}
/**
*
* @param Form $form
* @depends testAddAPersonPage
*/
public function testForgedNullGender(Form $form)
{
$form->get(self::FIRSTNAME_INPUT)->setValue('john');
$form->get(self::LASTNAME_INPUT)->setValue('doe');
$date = new \DateTime('1947-02-01');
$form->get(self::BIRTHDATE_INPUT)->setValue($date->format('d-m-Y'));
$this->client->submit($form);
$this->assertResponseStatusCodeSame(500);
}
/**
* Test the creation of a valid person.
@@ -140,8 +126,8 @@ class PersonControllerCreateTest extends WebTestCase
{
$this->fillAValidCreationForm($form);
$client = $this->client;
$client->submit($form);
$crawler = $client->submit($form);
$this->assertTrue((bool)$client->getResponse()->isRedirect(),
"a valid form redirect to url /{_locale}/person/{personId}/general/edit");
$client->followRedirect();

View File

@@ -20,18 +20,16 @@
namespace Chill\PersonBundle\Tests\Controller;
//ini_set('memory_limit', '-1');
use Chill\PersonBundle\Entity\Person;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\MainBundle\Test\PrepareClientTrait;
/**
* Test the edition of persons
*
* As I am logged in as "center a_social"
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class PersonControllerUpdateTest extends WebTestCase
{
@@ -71,8 +69,8 @@ class PersonControllerUpdateTest extends WebTestCase
$this->em->persist($this->person);
$this->em->flush();
$this->editUrl = '/en/person/'.$this->person->getId().'/general/edit';
$this->viewUrl = '/en/person/'.$this->person->getId().'/general';
$this->editUrl = '/fr/person/'.$this->person->getId().'/general/edit';
$this->viewUrl = '/fr/person/'.$this->person->getId().'/general';
$this->client = $this->getClientAuthenticated();
}
@@ -104,10 +102,10 @@ class PersonControllerUpdateTest extends WebTestCase
public function testHiddenFielsArePresent()
{
$crawler = $this->client->request('GET', $this->editUrl);
$configurables = array('placeOfBirth', 'phonenumber', 'email',
'countryOfBirth', 'nationality', 'spokenLanguages', 'maritalStatus');
$form = $crawler->selectButton('Submit')->form(); //;
$form = $crawler->selectButton('Enregistrer')->form(); //;
foreach($configurables as $key) {
$this->assertTrue($form->has('chill_personbundle_person['.$key.']'));
@@ -162,18 +160,18 @@ class PersonControllerUpdateTest extends WebTestCase
{
$crawler = $this->client->request('GET', $this->editUrl);
$form = $crawler->selectButton('Submit')
$form = $crawler->selectButton('Enregistrer')
->form();
//transform countries into value if needed
switch ($field) {
case 'nationality':
case 'countryOfBirth':
if ($value !== NULL) {
if (FALSE === empty($value)) {
$country = $this->em->getRepository('ChillMainBundle:Country')
->findOneByCountryCode($value);
$transformedValue = $country->getId();
} else {
$transformedValue = NULL;
$transformedValue = '';
}
break;
default:
@@ -208,7 +206,7 @@ class PersonControllerUpdateTest extends WebTestCase
$crawler = $this->client->request('GET', $this->editUrl);
$selectedLanguages = array('en', 'an', 'bbj');
$form = $crawler->selectButton('Submit')
$form = $crawler->selectButton('Enregistrer')
->form();
$form->get('chill_personbundle_person[spokenLanguages]')
->setValue($selectedLanguages);
@@ -238,7 +236,7 @@ class PersonControllerUpdateTest extends WebTestCase
{
$crawler = $this->client->request('GET', $this->editUrl);
$form = $crawler->selectButton('Submit')
$form = $crawler->selectButton('Enregistrer')
->form();
$form->get('chill_personbundle_person['.$field.']')
->setValue($value);
@@ -264,7 +262,7 @@ class PersonControllerUpdateTest extends WebTestCase
['lastName' , 'random Value', function(Person $person) { return $person->getLastName(); } ],
['placeOfBirth', 'none place', function(Person $person) { return $person->getPlaceOfBirth(); }],
['birthdate', '15-12-1980', function(Person $person) { return $person->getBirthdate()->format('d-m-Y'); }],
['phonenumber', '0123456789', function(Person $person) { return $person->getPhonenumber(); }],
['phonenumber', '+32123456789', function(Person $person) { return $person->getPhonenumber(); }],
['memo', 'jfkdlmq jkfldmsq jkmfdsq', function(Person $person) { return $person->getMemo(); }],
['countryOfBirth', 'BE', function(Person $person) { return $person->getCountryOfBirth()->getCountryCode(); }],
['nationality', 'FR', function(Person $person) { return $person->getNationality()->getCountryCode(); }],
@@ -275,7 +273,6 @@ class PersonControllerUpdateTest extends WebTestCase
['countryOfBirth', NULL, function(Person $person) { return $person->getCountryOfBirth(); }],
['nationality', NULL, function(Person $person) { return $person->getNationality(); }],
['gender', Person::FEMALE_GENDER, function(Person $person) { return $person->getGender(); }],
['maritalStatus', NULL, function(Person $person) {return $person->getMaritalStatus(); }]
);
}

View File

@@ -22,10 +22,6 @@ namespace Chill\PersonBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\Person;
/**
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @author Marc Ducobu <marc.ducobu@champs-libres.coop>
*/
class PersonControllerViewTestWithHiddenFields extends WebTestCase
{
/** @var \Doctrine\ORM\EntityManagerInterface The entity manager */
@@ -66,6 +62,7 @@ class PersonControllerViewTestWithHiddenFields extends WebTestCase
*/
public function testViewPerson()
{
$this->markTestSkipped("This configuration does not allow multiple environnements");
$client = static::createClient(
array('environment' => 'test_with_hidden_fields'),
array(

View File

@@ -0,0 +1,34 @@
<?php
namespace Bundle\ChillPersonBundle\Tests\Entity\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use PHPUnit\Framework\TestCase;
class HouseholdMemberTest extends TestCase
{
public function testPositionSharehousehold()
{
$position = (new Position())
->setShareHousehold(true)
;
$membership = (new HouseholdMember())
->setPosition($position)
;
$this->assertTrue($membership->getShareHousehold());
}
public function testPositionDoNotSharehousehold()
{
$position = (new Position())
->setShareHousehold(false)
;
$membership = (new HouseholdMember())
->setPosition($position)
;
$this->assertFalse($membership->getShareHousehold());
}
}

View File

@@ -26,7 +26,6 @@ use Chill\PersonBundle\Form\Type\PickPersonType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class PickPersonTypeTest extends KernelTestCase
{
@@ -59,6 +58,8 @@ class PickPersonTypeTest extends KernelTestCase
public function testWithoutOption()
{
$this->markTestSkipped("need to inject locale into url generator without request");
$form = $this->formFactory
->createBuilder(PickPersonType::class, null, array())
->getForm();
@@ -86,7 +87,8 @@ class PickPersonTypeTest extends KernelTestCase
*/
public function testWithOptionCenter()
{
$center = $this->container->get('doctrine.orm.entity_manager')
$this->markTestSkipped("need to inject locale into url generator without request");
$center = self::$container->get('doctrine.orm.entity_manager')
->getRepository('ChillMainBundle:Center')
->findOneBy(array('name' => 'Center A'))
;
@@ -117,7 +119,8 @@ class PickPersonTypeTest extends KernelTestCase
*/
public function testWithOptionCenters()
{
$centers = $this->container->get('doctrine.orm.entity_manager')
$this->markTestSkipped("need to inject locale into url generator without request");
$centers = self::$container->get('doctrine.orm.entity_manager')
->getRepository('ChillMainBundle:Center')
->findAll()
;
@@ -149,6 +152,7 @@ class PickPersonTypeTest extends KernelTestCase
public function testWithInvalidOptionCenters()
{
$this->markTestSkipped("need to inject locale into url generator without request");
$form = $this->formFactory
->createBuilder(PickPersonType::class, null, array(
'centers' => array('string')
@@ -158,6 +162,7 @@ class PickPersonTypeTest extends KernelTestCase
public function testWithOptionRoleInvalid()
{
$this->markTestSkipped("need to inject locale into url generator without request");
$form = $this->formFactory
->createBuilder(PickPersonType::class, null, array(
'role' => new \Symfony\Component\Security\Core\Role\Role('INVALID')

View File

@@ -0,0 +1,105 @@
<?php
namespace Chill\PersonBundle\Tests\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Household\MembersEditorFactory;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use PHPUnit\Framework\TestCase;
class MembersEditorTest extends TestCase
{
private MembersEditorFactory $factory;
protected function setUp()
{
$validator = $this->createMock(ValidatorInterface::class);
$this->factory = new MembersEditorFactory($validator);
}
public function testMovePersonWithSharedHousehold()
{
$person = new Person();
$position = (new Position())
->setShareHousehold(true)
;
$household1 = new Household();
$household2 = new Household();
$editor = $this->factory->createEditor($household1);
$editor->addMovement(
\DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01'),
$person,
$position);
$persistables = $editor->getPersistable();
$this->assertEquals(\count($persistables), 1);
$membership1 = $persistables[0];
$this->assertSame($household1, $membership1->getHousehold());
$this->assertNull($membership1->getEndDate());
// move to another household
$date = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-01-01');
$editor = $this->factory->createEditor($household2);
$editor->addMovement(
$date,
$person,
$position);
$persistables = $editor->getPersistable();
$this->assertEquals(1, count($persistables));
$membership2 = $persistables[0];
$this->assertSame($household2, $membership2->getHousehold());
$this->assertNull($membership2->getEndDate());
$this->assertNotNull($membership1->getEndDate(),
"assert that the membership1 is closed");
$this->assertEquals($date, $membership1->getEndDate());
}
public function testMovePersonWithoutSharedHousehold()
{
$person = new Person();
$position = (new Position())
->setShareHousehold(false)
;
$household1 = new Household();
$household2 = new Household();
$editor = $this->factory->createEditor($household1);
$editor->addMovement(
\DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01'),
$person,
$position);
$persistables = $editor->getPersistable();
$this->assertEquals(1, count($persistables));
$membership1 = $person->getHouseholdParticipations()->first();
$this->assertSame($household1, $membership1->getHousehold());
$this->assertNull($membership1->getEndDate());
// move to another household
$date = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-01-01');
$editor = $this->factory->createEditor($household2);
$editor->addMovement(
$date,
$person,
$position);
$persistables = $editor->getPersistable();
$this->assertEquals(1, count($persistables));
$membership2 = $person->getHouseholdParticipations()->last();
$this->assertNull($membership2->getEndDate());
$this->assertSame($household2, $membership2->getHousehold());
$this->assertNull($membership1->getEndDate(),
"assert that the membership1 is not closed");
}
}

View File

@@ -52,75 +52,75 @@ class PersonSearchTest extends WebTestCase
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByFirstName()
public function testSearchByLastName()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu');
$crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByFirstNameLower()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:depardieu');
$crawler = $this->generateCrawlerForSearch('@person firstname:Gérard');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByFirstNamePartim()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Dep');
$crawler = $this->generateCrawlerForSearch('@person firstname:Ger');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testFirstNameAccentued()
public function testLastNameAccentued()
{
$crawlerSpecial = $this->generateCrawlerForSearch('@person firstname:manço');
$crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:manço');
$this->assertRegExp('/Manço/', $crawlerSpecial->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person firstname:manco');
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:manco');
$this->assertRegExp('/Manço/', $crawlerNoSpecial->text());
}
public function testSearchByLastName()
public function testSearchByFirstName()
{
$crawler = $this->generateCrawlerForSearch('@person lastname:Jean');
$crawler = $this->generateCrawlerForSearch('@person firstname:Jean');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByLastNameLower()
public function testSearchByFirstNameLower2()
{
$crawler = $this->generateCrawlerForSearch('@person lastname:jean');
$crawler = $this->generateCrawlerForSearch('@person firstname:jean');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByLastNamePartim()
public function testSearchByFirstNamePartim2()
{
$crawler = $this->generateCrawlerForSearch('@person lastname:ean');
$crawler = $this->generateCrawlerForSearch('@person firstname:ean');
$this->assertRegExp('/Depardieu/', $crawler->text());
}
public function testSearchByLastNameAccented()
public function testSearchByFirstNameAccented()
{
$crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:Gérard');
$crawlerSpecial = $this->generateCrawlerForSearch('@person firstname:Gérard');
$this->assertRegExp('/Gérard/', $crawlerSpecial->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:Gerard');
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person firstname:Gerard');
$this->assertRegExp('/Gérard/', $crawlerNoSpecial->text());
}
public function testSearchCombineFirstnameAndNationality()
public function testSearchCombineLastnameAndNationality()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu nationality:RU');
$crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu nationality:RU');
$this->assertRegExp('/Gérard/', $crawler->text());
//if this is a AND clause, Jean Depardieu should not appears
@@ -130,7 +130,7 @@ class PersonSearchTest extends WebTestCase
public function testSearchCombineLastnameAndFirstName()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Depardieu lastname:Jean');
$crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu firstname:Jean');
$this->assertRegExp('/Depardieu/', $crawler->text());
//if this is a AND clause, Jean Depardieu should not appears
@@ -146,17 +146,17 @@ class PersonSearchTest extends WebTestCase
$this->assertRegExp('/Bart/', $crawler->text());
}
public function testSearchCombineBirthdateAndFirstName()
public function testSearchCombineBirthdateAndLastName()
{
$crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 firstname:(Van Snick)');
$crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 lastname:(Van Snick)');
$this->assertRegExp('/Bart/', $crawler->text());
$this->assertNotRegExp('/Depardieu/', $crawler->text());
}
public function testSearchCombineGenderAndFirstName()
public function testSearchCombineGenderAndLastName()
{
$crawler = $this->generateCrawlerForSearch('@person gender:woman firstname:(Depardieu)');
$crawler = $this->generateCrawlerForSearch('@person gender:woman lastname:(Depardieu)');
$this->assertRegExp('/Charline/', $crawler->text());
$this->assertNotRegExp('/Gérard/', $crawler->text());
@@ -171,8 +171,6 @@ class PersonSearchTest extends WebTestCase
$this->assertNotRegExp('/Jean/', $crawler->text());
}
public function testDefaultAccented()
{
$crawlerSpecial = $this->generateCrawlerForSearch('@person manço');
@@ -215,7 +213,7 @@ class PersonSearchTest extends WebTestCase
$client = $this->getAuthenticatedClient($username);
$crawler = $client->request('GET', '/fr/search', array(
'q' => $pattern
'q' => $pattern,
));
$this->assertTrue($client->getResponse()->isSuccessful());

View File

@@ -0,0 +1,48 @@
<?php
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class HouseholdNormalizerTest extends KernelTestCase
{
private ?NormalizerInterface $normalizer;
protected function setUp()
{
self::bootKernel();
$this->normalizer= self::$container->get(NormalizerInterface::class);
}
public function testNormalizationRecursive()
{
$person = new Person();
$member = new HouseholdMember();
$household = new Household();
$position = (new Position())
->setShareHousehold(true)
->setAllowHolder(true)
;
$member->setPerson($person)
->setStartDate(new \DateTimeImmutable('1 year ago'))
->setEndDate(new \DateTimeImmutable('1 month ago'));
$household->addMember($member);
$normalized = $this->normalizer->normalize($household,
'json', [ 'groups' => [ 'read' ]]);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('household', $normalized['type']);
}
}

View File

@@ -50,10 +50,10 @@ class TimelineAccompanyingPeriodTest extends \Chill\PersonBundle\Tests\Controlle
"the timeline page loads sucessfully");
$this->assertGreaterThan(0, $crawler->filter('.timeline div')->count(),
"the timeline page contains multiple div inside a .timeline element");
$this->assertContains("Ouverture d'une période d'accompagnement",
$this->assertContains(" Une période d'accompagnement est ouverte",
$crawler->filter('.timeline')->text(),
"the text 'une période d'accompagnement a été ouverte' is present");
$this->assertContains("Fermeture de la période d'accompagnement",
$this->assertContains("Une periode d'accompagnement se clôture",
$crawler->Filter('.timeline')->text(),
"the text 'Une période d'accompagnement a été fermée' is present");
}

View File

@@ -21,6 +21,14 @@ namespace Chill\PersonBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Doctrine\ORM\EntityManager;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\MainBundle\Entity\Center;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Security;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Timeline\TimelineSingleQuery;
/**
* Provide method to build timeline for accompanying periods
@@ -28,19 +36,22 @@ use Doctrine\ORM\EntityManager;
* This class is resued by TimelineAccompanyingPeriodOpening (for opening)
* and TimelineAccompanyingPeriodClosing (for closing)
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInterface
{
/**
*
* @var EntityManager
*/
protected $em;
protected EntityManager $em;
private Security $security;
private AuthorizationHelper $authorizationHelper;
private const SUPPORTED_CONTEXTS = [ 'person', 'center' ];
public function __construct(EntityManager $em)
public function __construct(EntityManager $em, Security $security, AuthorizationHelper $authorizationHelper)
{
$this->em = $em;
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
}
/**
@@ -72,23 +83,74 @@ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInt
*/
protected function basicFetchQuery($context, array $args)
{
if ($context !== 'person') {
if (FALSE === \in_array($context, self::SUPPORTED_CONTEXTS)) {
throw new \LogicException('TimelineAccompanyingPeriod is not able '
. 'to render context '.$context);
}
$metadata = $this->em
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod')
->getClassMetadata(AccompanyingPeriod::class)
;
return array(
'id' => $metadata->getColumnName('id'),
'FROM' => $metadata->getTableName(),
'WHERE' => sprintf('%s = %d',
$metadata
->getAssociationMapping('person')['joinColumns'][0]['name'],
$args['person']->getId())
);
[$where, $parameters] = $this->buildWhereClause($context, $args);
return TimelineSingleQuery::fromArray([
'id' => "{$metadata->getTableName()}.{$metadata->getColumnName('id')}",
'FROM' => $this->buildFromClause($context),
'WHERE' => $where,
'parameters' => $parameters
]);
}
private function buildFromClause($context)
{
$period = $this->em->getClassMetadata(AccompanyingPeriod::class);
$participation = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class);
$person = $this->em->getClassMetadata(Person::class);
$join = $participation->getAssociationMapping('accompanyingPeriod')['joinColumns'][0];
$joinPerson = $participation->getAssociationMapping('person')['joinColumns'][0];
if ($context === 'person') {
return "{$period->getTableName()} ".
"JOIN {$participation->getTableName()} ".
"ON {$participation->getTableName()}.{$join['name']} = ".
"{$period->getTableName()}.{$join['referencedColumnName']}";
} else {
return "{$period->getTableName()} ".
"JOIN {$participation->getTableName()} ".
"ON {$participation->getTableName()}.{$join['name']} = ".
"{$period->getTableName()}.{$join['referencedColumnName']} ".
"JOIN {$person->getTableName()} ".
"ON {$participation->getTableName()}.{$joinPerson['name']} = ".
"{$person->getTableName()}.{$joinPerson['referencedColumnName']}"
;
}
}
protected function buildWhereClause($context, array $args): array
{
$participation = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class);
$join = $participation->getAssociationMapping('person')['joinColumns'][0];
$person = $this->em->getClassMetadata(Person::class);
$joinCenter = $person->getAssociationMapping('center')['joinColumns'][0];
if ($context === 'center') {
$allowedCenters = $this->authorizationHelper->filterReachableCenters($this->security->getUser(), $args['centers'], PersonVoter::SEE);
$params = [];
$questionMarks = [];
$query = "{$person->getTableName()}.{$joinCenter['name']} IN (";
foreach ($allowedCenters as $c) {
$questionMarks[] = '?';
$params[] = $c->getId();
}
$query .= \implode(", ", $questionMarks).")";
return [$query, $params];
} elseif ($context === 'person') {
return [ "{$participation->getTableName()}.{$join['name']} = ?", [ $args['person']->getId() ]];
}
throw new \LogicException("this context is not supported: $context");
}
/**
@@ -104,7 +166,7 @@ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInt
{
return array(
'template' => $template,
'template_data' => ['person' => $args['person'], 'period' => $entity]
'template_data' => ['period' => $entity, 'context' => $context]
);
}
}

View File

@@ -21,11 +21,10 @@ namespace Chill\PersonBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Doctrine\ORM\EntityManager;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
/**
* Provide information for opening periods to timeline
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class TimelineAccompanyingPeriodClosing extends AbstractTimelineAccompanyingPeriod
{
@@ -46,20 +45,27 @@ class TimelineAccompanyingPeriodClosing extends AbstractTimelineAccompanyingPeri
public function fetchQuery($context, array $args)
{
$metadata = $this->em
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod');
->getClassMetadata(AccompanyingPeriod::class);
$data = $this->basicFetchQuery($context, $args);
$data['type'] = 'accompanying_period_closing';
$data['date'] = $metadata->getColumnName('closingDate');
$data['WHERE'] = sprintf('%s = %d AND %s IS NOT NULL',
$metadata
->getAssociationMapping('person')['joinColumns'][0]['name'],
$args['person']->getId(),
$metadata->getColumnName('closingDate'))
$query = $this->basicFetchQuery($context, $args);
[$where, $parameters] = $this->buildWhereClause($context, $args);
$query->setKey('accompanying_period_closing')
->setDate($metadata->getColumnName('closingDate'))
->setWhere($where)
->setParameters($parameters)
;
return $data;
return $query;
}
protected function buildWhereClause($context, array $args): array
{
list($query, $params) = parent::buildWhereClause($context, $args);
$period = $this->em->getClassMetadata(AccompanyingPeriod::class);
$query .= " AND {$period->getColumnName('closingDate')} IS NOT NULL ";
return [ $query, $params ];
}
/**

View File

@@ -21,11 +21,10 @@ namespace Chill\PersonBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Doctrine\ORM\EntityManager;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
/**
* Provide information for opening periods to timeline
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class TimelineAccompanyingPeriodOpening extends AbstractTimelineAccompanyingPeriod
{
@@ -46,14 +45,14 @@ class TimelineAccompanyingPeriodOpening extends AbstractTimelineAccompanyingPeri
public function fetchQuery($context, array $args)
{
$metadata = $this->em
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod');
->getClassMetadata(AccompanyingPeriod::class);
$data = $this->basicFetchQuery($context, $args);
$query = $this->basicFetchQuery($context, $args);
$data['type'] = 'accompanying_period_opening';
$data['date'] = $metadata->getColumnName('openingDate');
$query->setKey('accompanying_period_opening')
->setDate($metadata->getColumnName('openingDate'));
return $data;
return $query;
}
/**

View File

@@ -192,6 +192,25 @@ components:
text:
type: string
readOnly: true
Household:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- 'household'
HouseholdPosition:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- 'household_position'
paths:
/1.0/person/person/{id}.json:
@@ -764,3 +783,46 @@ paths:
description: "OK"
400:
description: "transition cannot be applyed"
/1.0/person/household/members/move.json:
post:
tags:
- household
summary: move one or multiple person from a household to another
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
concerned:
type: array
items:
type: object
properties:
person:
$ref: '#/components/schemas/PersonById'
start_date:
$ref: '#/components/schemas/Date'
position:
$ref: '#/components/schemas/HouseholdPosition'
holder:
type: boolean
comment:
type: string
destination:
oneOf:
- $ref: '#/components/schemas/Household'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"

View File

@@ -1,7 +1,7 @@
parameters:
# cl_chill_person.example.class: Chill\PersonBundle\Example
services:
services:
_defaults:
autowire: true
autoconfigure: true
@@ -24,18 +24,32 @@ services:
class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodOpening
arguments:
- "@doctrine.orm.entity_manager"
- '@Symfony\Component\Security\Core\Security'
- '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
public: true
tags:
- { name: chill.timeline, context: 'person' }
- { name: chill.timeline, context: 'center' }
chill.person.timeline.accompanying_period_closing:
class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodClosing
arguments:
- "@doctrine.orm.entity_manager"
- '@Symfony\Component\Security\Core\Security'
- '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
public: true
tags:
- { name: chill.timeline, context: 'person' }
- { name: chill.timeline, context: 'center' }
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
arguments:
@@ -48,3 +62,10 @@ services:
autoconfigure: true
resource: '../Repository/'
tags: ['doctrine.repository_service']
Chill\PersonBundle\Templating\Entity\:
autowire: true
autoconfigure: true
resource: '../Templating/Entity'
tags:
- 'chill.render_entity'

View File

@@ -16,6 +16,7 @@ services:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$timelineBuilder: '@chill_main.timeline_builder'
$paginatorFactory: '@chill_main.paginator_factory'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingPeriodController:

View File

@@ -1,5 +1,6 @@
services:
Chill\PersonBundle\DataFixtures\ORM\:
autowire: true
resource: ../../DataFixtures/ORM
tags: [ 'doctrine.fixture.orm' ]

View File

@@ -0,0 +1,3 @@
services:
Chill\PersonBundle\Household\MembersEditorFactory:
autowire: true

View File

@@ -1,17 +1,23 @@
services:
Chill\PersonBundle\Menu\SectionMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }
Chill\PersonBundle\Menu\AdminMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
Chill\PersonBundle\Menu\:
resource: './../../Menu'
autowire: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\SectionMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# $translator: '@Symfony\Component\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
# Chill\PersonBundle\Menu\AdminMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
arguments:
$showAccompanyingPeriod: '%chill_person.accompanying_period%'
@@ -19,8 +25,8 @@ services:
tags:
- { name: 'chill.menu_builder' }
Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
arguments:
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
# arguments:
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }

View File

@@ -1,24 +0,0 @@
services:
Chill\PersonBundle\Templating\Entity\:
resource: '../../Templating/Entity'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\PersonRender:
arguments:
$configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\ClosingMotiveRender:
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\SocialIssueRender:
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
$engine: '@Symfony\Component\Templating\EngineInterface'
tags:
- 'chill.render_entity'

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* prefix table concerning household with 'chill_person' and add constraints
*/
final class Version20210528092625 extends AbstractMigration
{
public function getDescription(): string
{
return 'prefix table concerning household with \'chill_person\' and add constraints';
}
public function up(Schema $schema): void
{
// we need to rename constraint, drop them first, recreate them after
$this->addSql('ALTER TABLE householdmembers DROP CONSTRAINT fk_4d1fb288e79ff843');
$this->addSql('ALTER TABLE householdmembers DROP CONSTRAINT fk_4d1fb288217bbb47');
// rename tables
$this->addSql('ALTER TABLE householdmembers RENAME TO chill_person_household_members');
$this->addSql('ALTER TABLE household RENAME TO chill_person_household');
// rename sequences
$this->addSql('ALTER SEQUENCE household_id_seq RENAME TO chill_person_household_id_seq');
$this->addSql('ALTER SEQUENCE householdmembers_id_seq RENAME TO chill_person_household_members_id_seq');
// recreate constraints
$this->addSql('ALTER TABLE chill_person_household_members ADD CONSTRAINT FK_EEF5DED7217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_household_members ADD CONSTRAINT FK_EEF5DED7E79FF843 FOREIGN KEY (household_id) REFERENCES chill_person_household (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
// create constraint 'householdmembers not overlaps'
$this->addSql('ALTER TABLE chill_person_household_members ADD CHECK (startdate < enddate)');
$this->addSql('ALTER TABLE chill_person_household_members ADD CONSTRAINT '.
"household_members_not_overlaps EXCLUDE USING GIST(
-- extension btree_gist required to include comparaison with integer
person_id WITH =,
daterange(startdate, enddate) WITH &&
) WHERE (sharedhousehold IS TRUE)
INITIALLY DEFERRED");
// rename constraints
$this->addSql('ALTER TABLE public.chill_person_household_to_addresses DROP CONSTRAINT fk_7109483e79ff843');
$this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_C28AF063E79FF843 FOREIGN KEY (household_id) REFERENCES chill_person_household (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
// rename indexes
$this->addSql('ALTER INDEX idx_7109483e79ff843 RENAME TO IDX_C28AF063E79FF843');
$this->addSql('ALTER INDEX idx_7109483f5b7af75 RENAME TO IDX_C28AF063F5B7AF75');
$this->addSql('ALTER INDEX idx_4d1fb288e79ff843 RENAME TO IDX_EEF5DED7E79FF843');
$this->addSql('ALTER INDEX idx_4d1fb288217bbb47 RENAME TO IDX_EEF5DED7217BBB47');
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException("the down method is not implemented");
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add position to househould_member
*/
final class Version20210528111624 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add position to househould_member';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_person_household_position_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_person_household_position (id INT NOT NULL, label JSON NOT NULL, shareHouseHold BOOLEAN NOT NULL, allowHolder BOOLEAN NOT NULL, ordering DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE chill_person_household_members ADD position_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_household_members DROP "position"');
$this->addSql('ALTER TABLE chill_person_household_members ADD CONSTRAINT FK_EEF5DED7DD842E46 FOREIGN KEY (position_id) REFERENCES chill_person_household_position (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_EEF5DED7DD842E46 ON chill_person_household_members (position_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_household_members DROP CONSTRAINT FK_EEF5DED7DD842E46');
$this->addSql('DROP SEQUENCE chill_person_household_position_id_seq CASCADE');
$this->addSql('DROP TABLE chill_person_household_position');
$this->addSql('ALTER TABLE chill_person_household_members ADD "position" VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_household_members DROP position_id');
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Household members: allow startdate and enddate to be null
*/
final class Version20210528132405 extends AbstractMigration
{
public function getDescription(): string
{
return 'Household members: allow startdate and enddate to be null';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_household_members ALTER startdate DROP NOT NULL');
$this->addSql('ALTER TABLE chill_person_household_members ALTER enddate DROP NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_household_members ALTER startDate SET NOT NULL');
$this->addSql('ALTER TABLE chill_person_household_members ALTER endDate SET NOT NULL');
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add house holder on membership
*/
final class Version20210528142121 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add house holder on membership';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_household_members ADD holder BOOLEAN DEFAULT FALSE NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_household_members DROP COLUMN holder');
}
}

View File

@@ -152,6 +152,8 @@ Update accompanying period: Mettre à jour une période d'accompagnement
'Closing motive': 'Motif de clôture'
'Person details': 'Détails de la personne'
'Update details for %name%': 'Modifier détails de %name%'
An accompanying period ends: Une periode d'accompagnement se clôture
An accompanying period starts: Une période d'accompagnement est ouverte
Any accompanying periods are open: Aucune période d'accompagnement ouverte
An accompanying period is open: Une période d'accompagnement est ouverte
Accompanying period list: Périodes d'accompagnement
@@ -165,6 +167,7 @@ Pediod closing form is not valid: Le formulaire n'est pas valide
Accompanying user: Accompagnant
No accompanying user: Aucun accompagnant
No data given: Pas d'information
Participants: Personnes impliquées
Create an accompanying course: Créer un parcours
This accompanying course is still a draft: Ce parcours est à l'état brouillon
Associated peoples: Usagers concernés
@@ -180,8 +183,6 @@ Referrer: Référent
# pickAPersonType
Pick a person: Choisir une personne
#address
Since %date%: Depuis le %date%
No address given: Pas d'adresse renseignée
The address has been successfully updated: L'adresse a été mise à jour avec succès
Update address for %name%: Mettre à jour une adresse pour %name%