Merge branch '139_demandeur' of gitlab.com:Chill-Projet/chill-bundles into 139_demandeur

This commit is contained in:
Mathieu Jaumotte 2021-05-19 16:22:28 +02:00
commit 1ecb1abc80
15 changed files with 348 additions and 108 deletions

View File

@ -5,7 +5,6 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -18,6 +17,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Symfony\Component\Workflow\Registry;
class AccompanyingCourseApiController extends ApiController class AccompanyingCourseApiController extends ApiController
{ {
@ -25,10 +25,37 @@ class AccompanyingCourseApiController extends ApiController
protected ValidatorInterface $validator; protected ValidatorInterface $validator;
public function __construct(EventDispatcherInterface $eventDispatcher, $validator) private Registry $registry;
{
public function __construct(
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator,
Registry $registry
) {
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->validator = $validator; $this->validator = $validator;
$this->registry = $registry;
}
public function confirmApi($id, Request $request, $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
$this->checkACL('confirm', $request, $_format, $accompanyingPeriod);
$workflow = $this->registry->get($accompanyingPeriod);
if (FALSE === $workflow->can($accompanyingPeriod, 'confirm')) {
throw new BadRequestException('It is not possible to confirm this period');
}
$workflow->apply($accompanyingPeriod, 'confirm');
$this->getDoctrine()->getManager()->flush();
return $this->json($accompanyingPeriod, Response::HTTP_OK, [], [
'groups' => [ 'read' ]
]);
} }
public function participationApi($id, Request $request, $_format) public function participationApi($id, Request $request, $_format)

View File

@ -6,6 +6,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@ -42,6 +43,41 @@ class AccompanyingCourseController extends Controller
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->validator = $validator; $this->validator = $validator;
} }
/**
* @Route("/{_locale}/person/parcours/new", name="chill_person_accompanying_course_new")
*/
public function newAction(Request $request): Response
{
$period = new AccompanyingPeriod();
$em = $this->getDoctrine()->getManager();
if ($request->query->has('person_id')) {
$personIds = $request->query->get('person_id');
if (FALSE === \is_array($personIds)) {
throw new BadRequestException("person_id parameter should be an array");
}
foreach ($personIds as $personId) {
$person = $em->getRepository(Person::class)->find($personId);
if (NULL !== $person) {
$period->addPerson($person);
}
}
}
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $period);
$em->persist($period);
$em->flush();
return $this->redirectToRoute('chill_person_accompanying_course_show', [
'accompanying_period_id' => $period->getId()
]);
}
/** /**
* Homepage of Accompanying Course section * Homepage of Accompanying Course section
* *
@ -86,83 +122,4 @@ class AccompanyingCourseController extends Controller
]); ]);
} }
/**
* Get API Data for showing endpoint
*
* @Route(
* "/{_locale}/person/api/1.0/accompanying-course/{accompanying_period_id}/show.{_format}",
* name="chill_person_accompanying_course_api_show"
* )
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})
*/
public function showAPI(AccompanyingPeriod $accompanyingCourse, $_format): Response
{
// TODO check ACL on AccompanyingPeriod
$this->dispatcher->dispatch(
AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT,
new AccompanyingPeriodPrivacyEvent($accompanyingCourse, [
'action' => 'showApi'
])
);
//$accompanyingCourse->getRequestorPerson();
//$accompanyingCourse->getRequestorThirdParty();
//$accompanyingCourse->isRequestorAnonymous();
//dump($accompanyingCourse); die;
switch ($_format) {
case 'json':
return $this->json($accompanyingCourse);
default:
throw new BadRequestException('Unsupported format');
}
}
/**
* Get API Data for showing endpoint
*
* @Route(
* "/{_locale}/person/api/1.0/accompanying-course/{accompanying_period_id}/participation.{_format}",
* name="chill_person_accompanying_course_api_add_participation",
* methods={"POST","DELETE"},
* format="json",
* requirements={
* "_format": "json",
* }
* )
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})
*/
public function participationAPI(Request $request, AccompanyingPeriod $accompanyingCourse, $_format): Response
{
switch ($_format) {
case 'json':
$person = $this->serializer->deserialize($request->getContent(), Person::class, $_format, [
]);
break;
default:
throw new BadRequestException('Unsupported format');
}
if (NULL === $person) {
throw new BadRequestException('person id not found');
}
// TODO add acl
$participation = ($request->getMethod() === 'POST') ?
$accompanyingCourse->addPerson($person) : $accompanyingCourse->removePerson($person);
$errors = $this->validator->validate($accompanyingCourse);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($participation);
}
} }

View File

@ -1,5 +1,4 @@
<?php <?php
/* /*
* Copyright (C) 2014-2016 Julien Fastré <julien.fastre@champs-libres.coop> * Copyright (C) 2014-2016 Julien Fastré <julien.fastre@champs-libres.coop>
* *
@ -166,6 +165,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$this->prependHomepageWidget($container); $this->prependHomepageWidget($container);
$this->prependDoctrineDQL($container); $this->prependDoctrineDQL($container);
$this->prependCruds($container); $this->prependCruds($container);
$this->prependWorkflows($container);
//add person_fields parameter as global //add person_fields parameter as global
$chillPersonConfig = $container->getExtensionConfig($this->getAlias()); $chillPersonConfig = $container->getExtensionConfig($this->getAlias());
@ -195,6 +195,39 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
)); ));
} }
protected function prependWorkflows(ContainerBuilder $container)
{
$container->prependExtensionConfig('framework', [
'workflows' => [
'accompanying_period_lifecycle' => [
'type' => 'state_machine',
'audit_trail' => [
'enabled' => true
],
'marking_store' => [
'type' => 'method',
'property' => 'step',
],
'supports' => [
'Chill\PersonBundle\Entity\AccompanyingPeriod'
],
'initial_marking' => 'DRAFT',
'places' => [
'DRAFT',
'CONFIRMED',
],
'transitions' => [
'confirm' => [
'from' => 'DRAFT',
'to' => 'CONFIRMED'
],
],
],
]
]);
}
/** /**
* Add a widget "add a person" on the homepage, automatically * Add a widget "add a person" on the homepage, automatically
* *
@ -406,6 +439,16 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
] ]
], ],
'confirm' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
]
],
] ]
], ],
[ [

View File

@ -266,8 +266,8 @@ class AccompanyingPeriod
* @param \DateTime $dateOpening * @param \DateTime $dateOpening
* @uses AccompanyingPeriod::setClosingDate() * @uses AccompanyingPeriod::setClosingDate()
*/ */
public function __construct(\DateTime $dateOpening) { public function __construct(\DateTime $dateOpening = null) {
$this->setOpeningDate($dateOpening); $this->setOpeningDate($dateOpening ?? new \DateTime('now'));
$this->participations = new ArrayCollection(); $this->participations = new ArrayCollection();
$this->scopes = new ArrayCollection(); $this->scopes = new ArrayCollection();
$this->socialIssues = new ArrayCollection(); $this->socialIssues = new ArrayCollection();

View File

@ -25,13 +25,14 @@ namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @ORM\Entity * @ORM\Entity(repositoryClass=ResourceRepository::class)
* @ORM\Table(name="chill_person_accompanying_period_resource") * @ORM\Table(name="chill_person_accompanying_period_resource")
* @DiscriminatorMap(typeProperty="type", mapping={ * @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period_resource"=Resource::class * "accompanying_period_resource"=Resource::class

View File

@ -23,8 +23,9 @@
namespace Chill\PersonBundle\Repository\AccompanyingPeriod; namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource; use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method Resource|null find($id, $lockMode = null, $lockVersion = null) * @method Resource|null find($id, $lockMode = null, $lockVersion = null)
@ -32,12 +33,12 @@ use Doctrine\ORM\EntityRepository;
* @method Resource[] findAll() * @method Resource[] findAll()
* @method Resource[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Resource[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
final class ResourceRepository final class ResourceRepository extends ServiceEntityRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(ManagerRegistry $registry)
{ {
$this->repository = $entityManager->getRepository(Resource::class); parent::__construct($registry, Resource::class);
} }
} }

View File

@ -22,8 +22,10 @@
<div class="grid-3"> <div class="grid-3">
<p style="text-align: right;"> <p style="text-align: right;">
<i>ouvert le {{ accompanyingCourse.openingDate|format_date('short') }}</i><br> <i>{{ 'Started on %date%'|trans({'%date%': accompanyingCourse.openingDate|format_date('short') } ) }}</i><br>
par <b>Soline Maillet | SIPAS</b> {% if accompanyingCourse.user is not null %}
par <b>{{ accompanyingCourse.user.usernameCanonical }}</b>
{% endif %}
</p> </p>
</div> </div>

View File

@ -38,6 +38,9 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRole
} }
// TODO take scopes into account // TODO take scopes into account
if (count($subject->getPersons()) === 0) {
return true;
}
foreach ($subject->getPersons() as $person) { foreach ($subject->getPersons() as $person) {
// give access as soon as on center is reachable // give access as soon as on center is reachable
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) { if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {

View File

@ -17,8 +17,11 @@ use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{ {
use DenormalizerAwareTrait; use DenormalizerAwareTrait;
use ObjectToPopulateTrait; use ObjectToPopulateTrait;
private ResourceRepository $repository;
public function __construct(ResourceRepository $repository) public function __construct(ResourceRepository $repository)
{ {
$this->repository = $repository; $this->repository = $repository;

View File

@ -43,6 +43,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
{ {
protected static EntityManagerInterface $em; protected static EntityManagerInterface $em;
protected ?int $personId = NULL;
protected ?AccompanyingPeriod $period = NULL;
/** /**
* Setup before the first test of this class (see phpunit doc) * Setup before the first test of this class (see phpunit doc)
*/ */
@ -302,6 +306,22 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertNull($period->getRequestor()); $this->assertNull($period->getRequestor());
} }
/**
* @dataProvider dataGenerateNewAccompanyingCourse
*/
public function testConfirm(AccompanyingPeriod $period)
{
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/confirm.json', $period->getId())
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
// add period to remove it in tear down
$this->period = $period;
}
/** /**
* *
* @dataProvider dataGenerateRandomAccompanyingCourse * @dataProvider dataGenerateRandomAccompanyingCourse
@ -439,25 +459,32 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
protected function tearDown() protected function tearDown()
{ {
// remove participation created during test 'testAccompanyingCourseAddParticipation'
// and if the test could not remove it
$testAddParticipationName = 'testAccompanyingCourseAddParticipation';
if ($testAddParticipationName !== \substr($this->getName(), 0, \strlen($testAddParticipationName))) {
return;
}
$em = static::$container->get(EntityManagerInterface::class); $em = static::$container->get(EntityManagerInterface::class);
$participation = $em // remove participation if set
->getRepository(AccompanyingPeriodParticipation::class) if ($this->personId && $this->period) {
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period]) $participation = $em
; ->getRepository(AccompanyingPeriodParticipation::class)
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
;
if (NULL !== $participation) { if (NULL !== $participation) {
$em->remove($participation); $em->remove($participation);
$em->flush(); $em->flush();
}
$this->personId = NULL;
$this->period = NULL;
} elseif ($this->period) {
$period = $em
->getRepository(AccompanyingPeriod::class)
->find($this->period->getId()) ;
if ($period !== NULL) {
$em->remove($period);
$em->flush();
}
$this->period = null;
} }
} }
@ -550,4 +577,38 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
} }
} }
public function dataGenerateNewAccompanyingCourse()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$period = new AccompanyingPeriod(new \DateTime('1 week ago'));
$user = $em->getRepository(User::class)
->findOneByUsernameCanonical('center a_social');
$period->setCreatedBy($user);
//$period->setCreatedAt(new \DateTime('yesterday'));
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
->setParameter('center', $center)
->setMaxResults(100)
->getScalarResult();
// create a random order
shuffle($personIds);
for ($i = 0; $i<2; $i++) {
$person = $em->getRepository(Person::class)->find(\array_pop($personIds));
$period->addPerson($person);
}
$em->persist($period);
$em->flush();
yield [ $period ];
}
} }

View File

@ -0,0 +1,88 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class AccompanyingCourseControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function setUp()
{
parent::setUp();
self::bootKernel();
$this->client = $this->getClientAuthenticated();
}
public function testNewWithoutUsers()
{
$this->client->request('GET', '/fr/person/parcours/new');
$this->assertResponseRedirects();
$location = $this->client->getResponse()->headers->get('Location');
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location));
}
/**
* @dataProvider dataGenerateRandomUsers
*/
public function testWithNewUsers($personId0, $personId1)
{
$this->client->request('GET', '/fr/person/parcours/new', [
'person_id' => [
$personId0,
$personId1
]
]);
$this->assertResponseRedirects();
$location = $this->client->getResponse()->headers->get('Location');
$matches = [];
$this->assertEquals(1, \preg_match("|^\/[^\/]+\/parcours/([\d]+)/show$|", $location, $matches));
$id = $matches[1];
$period = self::$container->get(EntityManagerInterface::class)
->getRepository(AccompanyingPeriod::class)
->find($id);
$this->assertNotNull($period);
$this->assertEquals(2, count($period->getParticipations()));
}
public function dataGenerateRandomUsers(): \Iterator
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$period = new AccompanyingPeriod(new \DateTime('1 week ago'));
$user = $em->getRepository(User::class)
->findOneByUsernameCanonical('center a_social');
$period->setCreatedBy($user);
//$period->setCreatedAt(new \DateTime('yesterday'));
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
->setParameter('center', $center)
->setMaxResults(100)
->getScalarResult();
yield [ \array_pop($personIds), \array_pop($personIds) ];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\PersonBundle\Tests\Workflows;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Workflow\Registry;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
class AccompanyingPeriodLifecycle extends KernelTestCase
{
protected function setUp()
{
self::bootKernel();
}
public function testConfirm()
{
$registry = self::$container->get(Registry::class);
$period = new AccompanyingPeriod(new \DateTime('now'));
$workflow = $registry->get($period);
$this->assertArrayHasKey('DRAFT', $workflow->getMarking($period)->getPlaces());
$this->assertTrue($workflow->can($period, 'confirm'));
}
}

View File

@ -677,3 +677,27 @@ paths:
description: "OK" description: "OK"
422: 422:
description: "object with validation errors" description: "object with validation errors"
/1.0/person/accompanying-course/{id}/confirm.json:
post:
tags:
- person
summary: confirm an accompanying course
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
400:
description: "transition cannot be applyed"

View File

@ -44,5 +44,7 @@ services:
- { name: validator.constraint_validator, alias: birthdate_not_before } - { name: validator.constraint_validator, alias: birthdate_not_before }
Chill\PersonBundle\Repository\: Chill\PersonBundle\Repository\:
autowire: true
autoconfigure: true
resource: '../Repository/' resource: '../Repository/'
tags: ['doctrine.repository_service'] tags: ['doctrine.repository_service']

View File

@ -51,4 +51,5 @@ services:
arguments: arguments:
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' $eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
$registry: '@Symfony\Component\Workflow\Registry'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']