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

This commit is contained in:
Jean-Francois Monfort 2021-05-20 08:57:32 +02:00
commit c59cf3f0df
128 changed files with 5772 additions and 745 deletions

View File

@ -73,7 +73,8 @@
"symfony/web-profiler-bundle": "^5.0", "symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*", "symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1", "symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2" "symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8"
}, },
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {

View File

@ -13,6 +13,9 @@ API
Chill provides a basic framework to build REST api. Chill provides a basic framework to build REST api.
Basic configuration
*******************
Configure a route Configure a route
================= =================
@ -34,7 +37,7 @@ You can also:
* `How to create your custom normalizer <https://symfony.com/doc/current/serializer/custom_normalizer.html>`_ * `How to create your custom normalizer <https://symfony.com/doc/current/serializer/custom_normalizer.html>`_
Auto-loading the routes Auto-loading the routes
*********************** =======================
Ensure that those lines are present in your file `app/config/routing.yml`: Ensure that those lines are present in your file `app/config/routing.yml`:
@ -47,7 +50,7 @@ Ensure that those lines are present in your file `app/config/routing.yml`:
Create your model Create your model
***************** =================
Create your model on the usual way: Create your model on the usual way:
@ -87,7 +90,7 @@ Create your model on the usual way:
Configure api Configure api
************* =============
Configure the api using Yaml (see the full configuration: :ref:`api_full_configuration`): Configure the api using Yaml (see the full configuration: :ref:`api_full_configuration`):
@ -171,7 +174,7 @@ Configure the api using Yaml (see the full configuration: :ref:`api_full_configu
} }
The :code:`_index` and :code:`_entity` action The :code:`_index` and :code:`_entity` action
============================================= *********************************************
The :code:`_index` and :code:`_entity` action are default actions: The :code:`_index` and :code:`_entity` action are default actions:
@ -189,7 +192,7 @@ Entity:
Path: :code:`/api/1.0/person/accompanying-period/origin/{id}.{_format}` Path: :code:`/api/1.0/person/accompanying-period/origin/{id}.{_format}`
Role Role
==== ****
By default, the key `base_role` is used to check ACL. Take care of creating the :code:`Voter` required to take that into account. By default, the key `base_role` is used to check ACL. Take care of creating the :code:`Voter` required to take that into account.
@ -216,7 +219,7 @@ You can also define a role for each method. In this case, this role is used for
HEAD: MY ROLE_SEE HEAD: MY ROLE_SEE
Customize the controller Customize the controller
======================== ************************
You can customize the controller by hooking into the default actions. Take care of extending :code:`Chill\MainBundle\CRUD\Controller\ApiController`. You can customize the controller by hooking into the default actions. Take care of extending :code:`Chill\MainBundle\CRUD\Controller\ApiController`.
@ -264,7 +267,7 @@ And set your controller in configuration:
HEAD: true HEAD: true
Create your own actions Create your own actions
======================= ***********************
You can add your own actions: You can add your own actions:
@ -361,8 +364,297 @@ Then, create the corresponding action into your controller:
} }
} }
Managing association
********************
ManyToOne association
=====================
In ManyToOne association, you can add associated entities using the :code:`PATCH` request. By default, the serializer deserialize entities only with their id and discriminator type, if any.
Example:
.. code-block:: bash
curl -X 'PATCH' \
'http://localhost:8001/api/1.0/person/accompanying-course/2668.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
# see the data sent to the server: \
-d '{
"type": "accompanying_period",
"id": 2668,
"origin": { "id": 11 }
}'
ManyToMany associations
=======================
In OneToMany association, you can easily create route for adding and removing entities, using :code:`POST` and :code:`DELETE` requests.
Prepare your entity, creating the methods :code:`addYourEntity` and :code:`removeYourEntity`:
.. code-block:: php
namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Entity\Scope;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* AccompanyingPeriod Class
*
* @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period"=AccompanyingPeriod::class
* })
*/
class AccompanyingPeriod
{
/**
* @var Collection
* @ORM\ManyToMany(
* targetEntity=Scope::class,
* cascade={}
* )
* @Groups({"read"})
*/
private $scopes;
public function addScope(Scope $scope): self
{
$this->scopes[] = $scope;
return $this;
}
public function removeScope(Scope $scope): void
{
$this->scopes->removeElement($scope);
}
Create your route into the configuration:
.. code-block:: yaml
chill_main:
apis:
-
class: Chill\PersonBundle\Entity\AccompanyingPeriod
name: accompanying_course
base_path: /api/1.0/person/accompanying-course
controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
actions:
scope:
methods:
POST: true
DELETE: true
GET: false
HEAD: false
PUT: false
PATCH: false
roles:
POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
GET: null
HEAD: null
PUT: null
PATCH: null
controller_action: null
path: null
single-collection: single
This will create a new route, which will accept two methods: DELETE and POST:
.. code-block:: raw
+--------------+---------------------------------------------------------------------------------------+
| Property | Value |
+--------------+---------------------------------------------------------------------------------------+
| Route Name | chill_api_single_accompanying_course_scope |
| Path | /api/1.0/person/accompanying-course/{id}/scope.{_format} |
| Path Regex | {^/api/1\.0/person/accompanying\-course/(?P<id>[^/]++)/scope\.(?P<_format>[^/]++)$}sD |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | POST|DELETE |
| Requirements | {id}: \d+ |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: csapi_accompanying_course_controller:scopeApi |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+---------------------------------------------------------------------------------------+
Then, create the controller action. Call the method:
.. code-block:: php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Entity\Scope;
class MyController extends ApiController
{
public function scopeApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('scope', $id, $request, $_format, 'scope', Scope::class, [ 'groups' => [ 'read' ] ]);
}
}
This will allow to add a scope by his id, and delete them.
Curl requests:
.. code-block:: bash
# add a scope with id 5
curl -X 'POST' \
'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"type": "scope",
"id": 5
}'
# remove a scope with id 5
curl -X 'DELETE' \
'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 5,
"type": "scope"
}'
Deserializing an association where multiple types are allowed
=============================================================
Sometimes, multiples types are allowed as association to one entity:
.. code-block:: php
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
class Resource
{
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class)
* @ORM\JoinColumn(nullable=true)
*/
private $thirdParty;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=true)
*/
private $person;
/**
*
* @param $resource Person|ThirdParty
*/
public function setResource($resource): self
{
// ...
}
/**
* @return ThirdParty|Person
* @Groups({"read", "write"})
*/
public function getResource()
{
return $this->person ?? $this->thirdParty;
}
}
This is not well taken into account by the Symfony serializer natively.
You must, then, create your own CustomNormalizer. You can help yourself using this:
.. code-block:: php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use Symfony\Component\Serializer\Exception;
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
public function __construct(ResourceRepository $repository)
{
$this->repository = $repository;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// .. snipped for brevity
if ($resource === NULL) {
$resource = new Resource();
}
if (\array_key_exists('resource', $data)) {
$res = $this->denormalizer->denormalize(
$data['resource'],
// call for a "multiple type"
DiscriminatedObjectDenormalizer::TYPE,
$format,
// into the context, we add the list of allowed types:
[
DiscriminatedObjectDenormalizer::ALLOWED_TYPES =>
[
Person::class, ThirdParty::class
]
]
);
$resource->setResource($res);
}
return $resource;
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return $type === Resource::class;
}
}
Serialization for collection Serialization for collection
============================ ****************************
A specific model has been defined for returning collection: A specific model has been defined for returning collection:
@ -381,8 +673,9 @@ A specific model has been defined for returning collection:
} }
} }
Where this is relevant, this model should be re-used in custom controller actions.
This can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination <pagination-ref>`). In custom actions, this can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination <pagination-ref>`).
.. code-block:: php .. code-block:: php
@ -400,10 +693,11 @@ This can be achieved quickly by assembling results into a :code:`Chill\MainBundl
} }
} }
.. _api_full_configuration: .. _api_full_configuration:
Full configuration example Full configuration example
========================== **************************
.. code-block:: yaml .. code-block:: yaml

View File

@ -82,7 +82,7 @@ Chill will be available at ``http://localhost:8001.`` Currently, there isn't any
.. code-block:: bash .. code-block:: bash
docker-compose exec --user $(id -u) php bin/console doctrine:fixtures:load docker-compose exec --user $(id -u) php bin/console doctrine:fixtures:load --purge-with-truncate
There are several users available: There are several users available:

View File

@ -18,11 +18,10 @@
<testsuite name="MainBundle"> <testsuite name="MainBundle">
<directory suffix="Test.php">src/Bundle/ChillMainBundle/Tests/</directory> <directory suffix="Test.php">src/Bundle/ChillMainBundle/Tests/</directory>
</testsuite> </testsuite>
<!--
<testsuite name="PersonBundle"> <testsuite name="PersonBundle">
<directory suffix="Test.php">src/Bundle/ChillPersonBundle/Tests/</directory> <directory suffix="Test.php">src/Bundle/ChillPersonBundle/Tests/</directory>
<exclude>src/Bundle/ChillPersonBundle/Tests/Export/*</exclude>
</testsuite> </testsuite>
-->
</testsuites> </testsuites>
<listeners> <listeners>

View File

@ -5,7 +5,7 @@ namespace Chill\MainBundle\CRUD\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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 Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface; use Chill\MainBundle\Pagination\PaginatorInterface;
@ -25,12 +25,19 @@ class AbstractCRUDController extends AbstractController
* *
* @param string $id * @param string $id
* @return object * @return object
* @throw Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the object is not found
*/ */
protected function getEntity($action, $id, Request $request): ?object protected function getEntity($action, $id, Request $request): object
{ {
return $this->getDoctrine() $e = $this->getDoctrine()
->getRepository($this->getEntityClass()) ->getRepository($this->getEntityClass())
->find($id); ->find($id);
if (NULL === $e) {
throw $this->createNotFoundException(sprintf("The object %s for id %s is not found", $this->getEntityClass(), $id));
}
return $e;
} }
/** /**
@ -222,4 +229,9 @@ class AbstractCRUDController extends AbstractController
{ {
return $this->container->get('chill_main.paginator_factory'); return $this->container->get('chill_main.paginator_factory');
} }
protected function getValidator(): ValidatorInterface
{
return $this->get('validator');
}
} }

View File

@ -8,6 +8,10 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Pagination\PaginatorInterface; use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
class ApiController extends AbstractCRUDController class ApiController extends AbstractCRUDController
{ {
@ -38,11 +42,6 @@ class ApiController extends AbstractCRUDController
return $postFetch; return $postFetch;
} }
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
}
$response = $this->checkACL($action, $request, $_format, $entity); $response = $this->checkACL($action, $request, $_format, $entity);
if ($response instanceof Response) { if ($response instanceof Response) {
return $response; return $response;
@ -81,13 +80,123 @@ class ApiController extends AbstractCRUDController
{ {
switch ($request->getMethod()) { switch ($request->getMethod()) {
case Request::METHOD_GET: case Request::METHOD_GET:
case REQUEST::METHOD_HEAD: case Request::METHOD_HEAD:
return $this->entityGet('_entity', $request, $id, $_format); return $this->entityGet('_entity', $request, $id, $_format);
case Request::METHOD_PUT:
case Request::METHOD_PATCH:
return $this->entityPut('_entity', $request, $id, $_format);
default: default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
} }
} }
public function entityPut($action, Request $request, $id, string $_format): Response
{
$entity = $this->getEntity($action, $id, $request, $_format);
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
if ($postFetch instanceof Response) {
return $postFetch;
}
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
}
$response = $this->checkACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
throw new BadRequestException("invalid json", 400, $e);
}
$errors = $this->validate($action, $request, $_format, $entity);
$response = $this->onAfterValidation($action, $request, $_format, $entity, $errors);
if ($response instanceof Response) {
return $response;
}
if ($errors->count() > 0) {
$response = $this->json($errors);
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
return $response;
}
$this->getDoctrine()->getManager()->flush();
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
if ($response instanceof Response) {
return $response;
}
return $this->json(
$entity,
Response::HTTP_OK,
[],
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity)
);
}
protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response
{
return null;
}
protected function onAfterFlush(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response
{
return null;
}
protected function getValidationGroups(string $action, Request $request, string $_format, $entity): ?array
{
return null;
}
protected function validate(string $action, Request $request, string $_format, $entity, array $more = []): ConstraintViolationListInterface
{
$validationGroups = $this->getValidationGroups($action, $request, $_format, $entity);
return $this->getValidator()->validate($entity, null, $validationGroups);
}
/**
* Deserialize the content of the request into the class associated with the curd
*/
protected function deserialize(string $action, Request $request, string $_format, $entity = null): object
{
$default = [];
if (NULL !== $entity) {
$default[AbstractNormalizer::OBJECT_TO_POPULATE] = $entity;
}
$context = \array_merge(
$default,
$this->getContextForSerialization($action, $request, $_format, $entity)
);
return $this->getSerializer()->deserialize($request->getContent(), $this->getEntityClass(), $_format, $context);
}
/** /**
* Base action for indexing entities * Base action for indexing entities
*/ */
@ -173,6 +282,110 @@ class ApiController extends AbstractCRUDController
return $this->serializeCollection($action, $request, $_format, $paginator, $entities); return $this->serializeCollection($action, $request, $_format, $paginator, $entities);
} }
/**
* Add or remove an associated entity, using `add` and `remove` methods.
*
* This method:
*
* 1. Fetch the base entity (throw 404 if not found)
* 2. checkACL,
* 3. run onPostCheckACL, return response if any,
* 4. deserialize posted data into the entity given by $postedDataType, with the context in $postedDataContext
* 5. run 'add+$property' for POST method, or 'remove+$property' for DELETE method
* 6. validate the base entity (not the deserialized one). Groups are fetched from getValidationGroups, validation is perform by `validate`
* 7. run onAfterValidation
* 8. if errors, return a 422 response with errors
* 9. flush the data
* 10. run onAfterFlush
* 11. return a 202 response for DELETE with empty body, or HTTP 200 for post with serialized posted entity
*
* @param string action
* @param mixed id
* @param Request $request
* @param string $_format
* @param string $property the name of the property. This will be used to make a `add+$property` and `remove+$property` method
* @param string $postedDataType the type of the posted data (the content)
* @param string $postedDataContext a context to deserialize posted data (the content)
* @throw BadRequestException if unable to deserialize the posted data
* @throw BadRequestException if the method is not POST or DELETE
*
*/
protected function addRemoveSomething(string $action, $id, Request $request, string $_format, string $property, string $postedDataType, $postedDataContext = []): Response
{
$entity = $this->getEntity($action, $id, $request);
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
if ($postFetch instanceof Response) {
return $postFetch;
}
$response = $this->checkACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
try {
$postedData = $this->getSerializer()->deserialize($request->getContent(), $postedDataType, $_format, $postedDataContext);
} catch (\Symfony\Component\Serializer\Exception\UnexpectedValueException $e) {
throw new BadRequestException(sprintf("Unable to deserialize posted ".
"data: %s", $e->getMessage()), 0, $e);
}
switch ($request->getMethod()) {
case Request::METHOD_DELETE:
// oups... how to use property accessor to remove element ?
$entity->{'remove'.\ucfirst($property)}($postedData);
break;
case Request::METHOD_POST:
$entity->{'add'.\ucfirst($property)}($postedData);
break;
default:
throw new BadRequestException("this method is not supported");
}
$errors = $this->validate($action, $request, $_format, $entity, [$postedData]);
$response = $this->onAfterValidation($action, $request, $_format, $entity, $errors, [$postedData]);
if ($response instanceof Response) {
return $response;
}
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors, 422);
}
$this->getDoctrine()->getManager()->flush();
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors, [$postedData]);
if ($response instanceof Response) {
return $response;
}
switch ($request->getMethod()) {
case Request::METHOD_DELETE:
return $this->json('', Response::HTTP_OK);
case Request::METHOD_POST:
return $this->json(
$postedData,
Response::HTTP_OK,
[],
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity, [$postedData])
);
}
}
/** /**
* Serialize collections * Serialize collections
* *
@ -189,7 +402,26 @@ class ApiController extends AbstractCRUDController
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
{ {
return []; switch ($request->getMethod()) {
case Request::METHOD_GET:
return [ 'groups' => [ 'read' ]];
case Request::METHOD_PUT:
case Request::METHOD_PATCH:
return [ 'groups' => [ 'write' ]];
default:
throw new \LogicException("get context for serialization is not implemented for this method");
}
}
/**
* Get the context for serialization post alter query (in case of
* PATCH, PUT, or POST method)
*
* This is called **after** the entity was altered.
*/
protected function getContextForSerializationPostAlter(string $action, Request $request, string $_format, $entity, array $more = []): array
{
return [ 'groups' => [ 'read' ]];
} }
/** /**

View File

@ -22,6 +22,7 @@
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Search\UnknowSearchDomainException; use Chill\MainBundle\Search\UnknowSearchDomainException;
@ -34,6 +35,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Chill\MainBundle\Search\SearchProvider; use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi;
/** /**
* Class SearchController * Class SearchController
@ -42,32 +44,24 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
*/ */
class SearchController extends AbstractController class SearchController extends AbstractController
{ {
/** protected SearchProvider $searchProvider;
*
* @var SearchProvider
*/
protected $searchProvider;
/** protected TranslatorInterface $translator;
*
* @var TranslatorInterface
*/
protected $translator;
/** protected PaginatorFactory $paginatorFactory;
*
* @var PaginatorFactory protected SearchApi $searchApi;
*/
protected $paginatorFactory;
function __construct( function __construct(
SearchProvider $searchProvider, SearchProvider $searchProvider,
TranslatorInterface $translator, TranslatorInterface $translator,
PaginatorFactory $paginatorFactory PaginatorFactory $paginatorFactory,
SearchApi $searchApi
) { ) {
$this->searchProvider = $searchProvider; $this->searchProvider = $searchProvider;
$this->translator = $translator; $this->translator = $translator;
$this->paginatorFactory = $paginatorFactory; $this->paginatorFactory = $paginatorFactory;
$this->searchApi = $searchApi;
} }
@ -153,6 +147,19 @@ class SearchController extends AbstractController
); );
} }
public function searchApi(Request $request, $_format): JsonResponse
{
//TODO this is an incomplete implementation
$query = $request->query->get('q', '');
$results = $this->searchApi->getResults($query, 0, 150);
$paginator = $this->paginatorFactory->create(count($results));
$collection = new Collection($results, $paginator);
return $this->json($collection);
}
public function advancedSearchListAction(Request $request) public function advancedSearchListAction(Request $request)
{ {
/* @var $variable Chill\MainBundle\Search\SearchProvider */ /* @var $variable Chill\MainBundle\Search\SearchProvider */

View File

@ -55,11 +55,11 @@ class LoadCenters extends AbstractFixture implements OrderedFixtureInterface
public function load(ObjectManager $manager) public function load(ObjectManager $manager)
{ {
foreach (static::$centers as $new) { foreach (static::$centers as $new) {
$centerA = new Center(); $center = new Center();
$centerA->setName($new['name']); $center->setName($new['name']);
$manager->persist($centerA); $manager->persist($center);
$this->addReference($new['ref'], $centerA); $this->addReference($new['ref'], $center);
static::$refs[] = $new['ref']; static::$refs[] = $new['ref'];
} }

View File

@ -35,6 +35,7 @@ use Chill\MainBundle\Doctrine\DQL\OverlapsI;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Chill\MainBundle\Doctrine\DQL\Replace; use Chill\MainBundle\Doctrine\DQL\Replace;
use Symfony\Component\HttpFoundation\Request;
/** /**
* Class ChillMainExtension * Class ChillMainExtension
@ -212,6 +213,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->prependExtensionConfig('monolog', array( $container->prependExtensionConfig('monolog', array(
'channels' => array('chill') 'channels' => array('chill')
)); ));
//add crud api
$this->prependCruds($container);
} }
/** /**
@ -235,4 +239,97 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
// Note: the controller are loaded inside compiler pass // Note: the controller are loaded inside compiler pass
} }
/**
* @param ContainerBuilder $container
*/
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'apis' => [
[
'class' => \Chill\MainBundle\Entity\Address::class,
'name' => 'address',
'base_path' => '/api/1.0/main/address',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_POST => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\AddressReference::class,
'name' => 'address_reference',
'base_path' => '/api/1.0/main/address-reference',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\PostalCode::class,
'name' => 'postal_code',
'base_path' => '/api/1.0/main/postal-code',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\Country::class,
'name' => 'country',
'base_path' => '/api/1.0/main/country',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
]
]
]);
}
} }

View File

@ -221,6 +221,7 @@ class Configuration implements ConfigurationInterface
->booleanNode(Request::METHOD_POST)->defaultFalse()->end() ->booleanNode(Request::METHOD_POST)->defaultFalse()->end()
->booleanNode(Request::METHOD_DELETE)->defaultFalse()->end() ->booleanNode(Request::METHOD_DELETE)->defaultFalse()->end()
->booleanNode(Request::METHOD_PUT)->defaultFalse()->end() ->booleanNode(Request::METHOD_PUT)->defaultFalse()->end()
->booleanNode(Request::METHOD_PATCH)->defaultFalse()->end()
->end() ->end()
->end() ->end()
->arrayNode('roles') ->arrayNode('roles')
@ -232,6 +233,7 @@ class Configuration implements ConfigurationInterface
->scalarNode(Request::METHOD_POST)->defaultNull()->end() ->scalarNode(Request::METHOD_POST)->defaultNull()->end()
->scalarNode(Request::METHOD_DELETE)->defaultNull()->end() ->scalarNode(Request::METHOD_DELETE)->defaultNull()->end()
->scalarNode(Request::METHOD_PUT)->defaultNull()->end() ->scalarNode(Request::METHOD_PUT)->defaultNull()->end()
->scalarNode(Request::METHOD_PATCH)->defaultNull()->end()
->end() ->end()
->end() ->end()
->end() ->end()

View File

@ -0,0 +1,65 @@
<?php
namespace Chill\MainBundle\Doctrine\Event;
use Chill\MainBundle\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Symfony\Component\Security\Core\Security;
class TrackCreateUpdateSubscriber implements EventSubscriber
{
private Security $security;
/**
* @param Security $security
*/
public function __construct(Security $security)
{
$this->security = $security;
}
/**
* {@inheritDoc}
*/
public function getSubscribedEvents()
{
return [
Events::prePersist,
Events::preUpdate
];
}
public function prePersist(LifecycleEventArgs $args): void
{
$object = $args->getObject();
if ($object instanceof TrackCreationInterface
&& $this->security->getUser() instanceof User) {
$object->setCreatedBy($this->security->getUser());
$object->setCreatedAt(new \DateTimeImmutable('now'));
}
$this->onUpdate($object);
}
public function preUpdate(LifecycleEventArgs $args): void
{
$object = $args->getObject();
$this->onUpdate($object);
}
protected function onUpdate(object $object): void
{
if ($object instanceof TrackUpdateInterface
&& $this->security->getUser() instanceof User) {
$object->setUpdatedBy($this->security->getUser());
$object->setUpdatedAt(new \DateTimeImmutable('now'));
}
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Chill\MainBundle\Doctrine\Model;
use Chill\MainBundle\Entity\User;
interface TrackCreationInterface
{
public function setCreatedBy(User $user): self;
public function setCreatedAt(\DateTimeInterface $datetime): self;
}

View File

@ -0,0 +1,12 @@
<?php
namespace Chill\MainBundle\Doctrine\Model;
use Chill\MainBundle\Entity\User;
interface TrackUpdateInterface
{
public function setUpdatedBy(User $user): self;
public function setUpdatedAt(\DateTimeInterface $datetime): self;
}

View File

@ -2,12 +2,11 @@
namespace Chill\MainBundle\Entity; namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Entity\AddressReferenceRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Doctrine\Model\Point; use Chill\MainBundle\Doctrine\Model\Point;
/** /**
* @ORM\Entity(repositoryClass=AddressReferenceRepository::class) * @ORM\Entity()
* @ORM\Table(name="chill_main_address_reference") * @ORM\Table(name="chill_main_address_reference")
* @ORM\HasLifecycleCallbacks() * @ORM\HasLifecycleCallbacks()
*/ */

View File

@ -24,13 +24,16 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Chill\MainBundle\Entity\RoleScope; use Chill\MainBundle\Entity\RoleScope;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* @ORM\Entity() * @ORM\Entity()
* @ORM\Table(name="scopes") * @ORM\Table(name="scopes")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region") * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
* * @DiscriminatorMap(typeProperty="type", mapping={
* @author Julien Fastré <julien.fastre@champs-libres.coop> * "scope"=Scope::class
* })
*/ */
class Scope class Scope
{ {
@ -40,6 +43,7 @@ class Scope
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @Groups({"read"})
*/ */
private $id; private $id;
@ -49,6 +53,7 @@ class Scope
* @var array * @var array
* *
* @ORM\Column(type="json_array") * @ORM\Column(type="json_array")
* @Groups({"read"})
*/ */
private $name = []; private $name = [];

View File

@ -7,6 +7,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* User * User
@ -14,6 +15,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* @ORM\Entity(repositoryClass="Chill\MainBundle\Repository\UserRepository") * @ORM\Entity(repositoryClass="Chill\MainBundle\Repository\UserRepository")
* @ORM\Table(name="users") * @ORM\Table(name="users")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region") * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
* @DiscriminatorMap(typeProperty="type", mapping={
* "user"=User::class
* })
*/ */
class User implements AdvancedUserInterface { class User implements AdvancedUserInterface {

View File

@ -39,8 +39,17 @@ div.subheader {
height: 130px; height: 130px;
} }
//// VUEJS //// //// SCRATCH BUTTONS
.sc-button {
&.disabled {
cursor: default;
&.bt-remove {
background-color: #d9d9d9;
}
}
}
//// VUEJS ////
div.vue-component { div.vue-component {
padding: 1.5em; padding: 1.5em;
margin: 2em 0; margin: 2em 0;
@ -95,7 +104,9 @@ div.vue-component {
} }
//// AddPersons modal //// AddPersons modal
div.modal-body.up { div.body-head {
overflow-y: unset;
div.modal-body:first-child {
margin: auto 4em; margin: auto 4em;
div.search { div.search {
position: relative; position: relative;
@ -105,23 +116,35 @@ div.modal-body.up {
} }
i { i {
position: absolute; position: absolute;
top: 50%;
left: 0.5em;
padding: 0.65em 0;
opacity: 0.5; opacity: 0.5;
padding: 0.65em 0;
top: 50%;
} }
i.fa-search {
left: 0.5em;
}
i.fa-times {
right: 1em;
padding: 0.75em 0;
cursor: pointer;
}
}
}
div.modal-body:last-child {
padding-bottom: 0;
} }
} }
div.results {
div.count { div.count {
margin: -0.5em 0 0.7em; margin: -0.5em 0 0.7em;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
a {
cursor: pointer;
} }
}
div.results {
div.list-item { div.list-item {
line-height: 26pt; padding: 0.4em 0.8em;
padding: 0.3em 0.8em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
&.checked { &.checked {
@ -132,11 +155,20 @@ div.results {
& > input { & > input {
margin-right: 0.8em; margin-right: 0.8em;
} }
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
} }
div.right_actions { div.right_actions {
margin: 0 0 0 auto; margin: 0 0 0 auto;
display: flex;
align-items: flex-end;
& > * { & > * {
margin-left: 0.5em; margin-left: 0.5em;
align-self: baseline;
} }
a.sc-button { a.sc-button {
border: 1px solid lightgrey; border: 1px solid lightgrey;
@ -146,8 +178,19 @@ div.results {
} }
} }
} }
.discret { .discret {
color: grey; color: grey;
margin-right: 1em; margin-right: 1em;
} }
a.flag-toggle {
color: white;
padding: 0 10px;
cursor: pointer;
&:hover {
color: white;
//border: 1px solid rgba(255,255,255,0.2);
text-decoration: underline;
border-radius: 20px;
}
}

View File

@ -0,0 +1,41 @@
<template>
<div v-if="address.address">
{{ address.address.street }}, {{ address.address.streetNumber }}
</div>
<div v-if="address.city">
{{ address.city.code }} {{ address.city.name }}
</div>
<div v-if="address.country">
{{ address.country.name }}
</div>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</template>
<script>
import { mapState } from 'vuex';
import AddAddress from '../_components/AddAddress.vue';
export default {
name: 'App',
components: {
AddAddress
},
computed: {
address() {
return this.$store.state.address;
}
},
methods: {
addNewAddress({ address, modal }) {
console.log('@@@ CLICK button addNewAdress', address);
this.$store.dispatch('addAddress', address.selected);
modal.showModal = false;
}
}
};
</script>

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { addressMessages } from './js/i18n'
import { store } from './store'
import App from './App.vue';
const i18n = _createI18n(addressMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#address');

View File

@ -0,0 +1,22 @@
const addressMessages = {
fr: {
add_an_address: 'Ajouter une adresse',
select_an_address: 'Sélectionner une adresse',
fill_an_address: 'Compléter l\'adresse',
select_country: 'Choisir le pays',
select_city: 'Choisir une localité',
select_address: 'Choisir une adresse',
isNoAddress: 'L\'adresse n\'est pas celle d\'un domicile fixe ?',
floor: 'Étage',
corridor: 'Couloir',
steps: 'Escalier',
flat: 'Appartement',
buildingName: 'Nom du batiment',
extra: 'Complément d\'adresse',
distribution: 'Service particulier de distribution'
}
};
export {
addressMessages
};

View File

@ -0,0 +1,43 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
// le fetch POST serait rangé dans la logique du composant qui appelle AddAddress
//import { postAddress } from '... api'
const debug = process.env.NODE_ENV !== 'production';
const store = createStore({
strict: debug,
state: {
address: {},
errorMsg: {}
},
getters: {
},
mutations: {
addAddress(state, address) {
console.log('@M addAddress address', address);
state.address = address;
}
},
actions: {
addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload);
commit('addAddress', payload); // à remplacer par
// fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok.
// La confirmation est l'adresse elle-même.
//
// postAddress(payload)
// .fetch(address => new Promise((resolve, reject) => {
// commit('addAddress', address);
// resolve();
// }))
// .catch((error) => {
// state.errorMsg.push(error.message);
// });
}
}
});
export { store };

View File

@ -0,0 +1,46 @@
/*
* Endpoint countries GET
* TODO
*/
const fetchCountries = () => {
console.log('<<< fetching countries');
return [
{id: 1, name: 'France', countryCode: 'FR'},
{id: 2, name: 'Belgium', countryCode: 'BE'}
];
};
/*
* Endpoint cities GET
* TODO
*/
const fetchCities = (country) => {
console.log('<<< fetching cities for', country);
return [
{id: 1, name: 'Bruxelles', code: '1000', country: 'BE'},
{id: 2, name: 'Aisne', code: '85045', country: 'FR'},
{id: 3, name: 'Saint-Gervais', code: '85230', country: 'FR'}
];
};
/*
* Endpoint chill_main_address_reference_api_show
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference object
*/
const fetchReferenceAddresses = (city) => {
console.log('<<< fetching references addresses for', city); // city n'est pas utilisé pour le moment
const url = `/api/1.0/main/address-reference.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
fetchCountries,
fetchCities,
fetchReferenceAddresses
};

View File

@ -0,0 +1,219 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_an_address') }}
</button>
<teleport to="body">
<modal v-if="modal.showModal"
v-bind:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('add_an_address') }}</h3>
</template>
<template v-slot:body>
<h4>{{ $t('select_an_address') }}</h4>
<label for="isNoAddress">
<input type="checkbox"
name="isNoAddress"
v-bind:placeholder="$t('isNoAddress')"
v-model="isNoAddress"
v-bind:value="value"/>
{{ $t('isNoAddress') }}
</label>
<country-selection
v-bind:address="address"
v-bind:getCities="getCities">
</country-selection>
<city-selection
v-bind:address="address"
v-bind:getReferenceAddresses="getReferenceAddresses">
</city-selection>
<address-selection
v-bind:address="address"
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
<address-map
v-bind:address="address"
ref="addressMap">
</address-map>
<address-more
v-if="!isNoAddress"
v-bind:address="address">
</address-more>
<!--
<div class="address_form__fields__isNoAddress"></div>
<div class="address_form__select">
<div class="address_form__select__header"></div>
<div class="address_form__select__left"></div>
<div class="address_form__map"></div>
</div>
<div class="address_form__fields">
<div class="address_form__fields__header"></div>
<div class="address_form__fields__left"></div>
<div class="address_form__fields__right"></div>
</div>
à discuter,
mais je pense qu'il est préférable de profiter de l'imbriquation des classes css
div.address_form {
div.select {
div.header {}
div.left {}
div.map {}
}
}
-->
</template>
<template v-slot:footer>
<button class="sc-button green"
@click.prevent="$emit('addNewAddress', { address, modal })">
<i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}}
</button>
</template>
</modal>
</teleport>
</template>
<script>
import Modal from './Modal';
import { fetchCountries, fetchCities, fetchReferenceAddresses } from '../_api/AddAddress'
import CountrySelection from './AddAddress/CountrySelection';
import CitySelection from './AddAddress/CitySelection';
import AddressSelection from './AddAddress/AddressSelection';
import AddressMap from './AddAddress/AddressMap';
import AddressMore from './AddAddress/AddressMore'
export default {
name: 'AddAddresses',
components: {
Modal,
CountrySelection,
CitySelection,
AddressSelection,
AddressMap,
AddressMore
},
props: [
],
emits: ['addNewAddress'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
address: {
loaded: {
countries: [],
cities: [],
addresses: [],
},
selected: {
country: {},
city: {},
address: {},
},
addressMap: {
center : [48.8589, 2.3469], // Note: LeafletJs demands [lat, lon] cfr https://macwright.com/lonlat/
zoom: 12
},
isNoAddress: false,
floor: null,
corridor: null,
steps: null,
floor: null,
flat: null,
buildingName: null,
extra: null,
distribution: null,
},
errorMsg: {}
}
},
computed: {
isNoAddress: {
set(value) {
console.log('value', value);
this.address.isNoAddress = value;
},
get() {
return this.address.isNoAddress;
}
}
},
methods: {
openModal() {
this.modal.showModal = true;
this.resetAll();
this.getCountries();
//this.$nextTick(function() {
// this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale
//})
},
getCountries() {
console.log('getCountries');
this.address.loaded.countries = fetchCountries(); // à remplacer par
// fetchCountries().then(countries => new Promise((resolve, reject) => {
// this.address.loaded.countries = countries;
// resolve()
// }))
// .catch((error) => {
// this.errorMsg.push(error.message);
// });
},
getCities(country) {
console.log('getCities for', country.name);
this.address.loaded.cities = fetchCities(); // à remplacer par
// fetchCities(country).then(cities => new Promise((resolve, reject) => {
// this.address.loaded.cities = cities;
// resolve()
// }))
// .catch((error) => {
// this.errorMsg.push(error.message);
// });
},
getReferenceAddresses(city) {
console.log('getReferenceAddresses for', city.name);
fetchReferenceAddresses(city) // il me semble que le paramètre city va limiter le poids des adresses de références reçues
.then(addresses => new Promise((resolve, reject) => {
console.log('addresses', addresses);
this.address.loaded.addresses = addresses.results;
resolve();
}))
.catch((error) => {
this.errorMsg.push(error.message);
});
},
updateMapCenter(point) {
console.log('point', point);
this.address.addressMap.center[0] = point.coordinates[1]; // TODO use reverse()
this.address.addressMap.center[1] = point.coordinates[0];
this.$refs.addressMap.update(); // cast child methods
},
resetAll() {
console.log('reset all selected');
this.address.loaded.addresses = [];
this.address.selected.address = {};
this.address.loaded.cities = [];
this.address.selected.city = {};
this.address.selected.country = {};
console.log('cities and addresses', this.address.loaded.cities, this.address.loaded.addresses);
}
}
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="container">
<div id='address_map' style='height:400px; width:400px;'></div>
</div>
</template>
<script>
import L from 'leaflet';
import markerIconPng from 'leaflet/dist/images/marker-icon.png'
import 'leaflet/dist/leaflet.css';
let map;
export default {
name: 'AddressMap',
props: ['address'],
computed: {
center() {
return this.address.addressMap.center;
},
},
methods:{
init() {
map = L.map('address_map').setView([48.8589, 2.3469], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
});
L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map);
},
update() {
console.log('update map with : ', this.address.addressMap.center)
map.setView(this.address.addressMap.center, 12);
}
},
mounted(){
this.init()
}
}
</script>

View File

@ -0,0 +1,112 @@
<template>
<div>
<h4>{{ $t('fill_an_address') }}</h4>
<input
type="text"
name="floor"
:placeholder="$t('floor')"
v-model="floor"/>
<input
type="text"
name="corridor"
:placeholder="$t('corridor')"
v-model="corridor"/>
<input
type="text"
name="steps"
:placeholder="$t('steps')"
v-model="steps"/>
<input
type="text"
name="flat"
:placeholder="$t('flat')"
v-model="flat"/>
<input
type="text"
name="buildingName"
:placeholder="$t('buildingName')"
v-model="buildingName"/>
<input
type="text"
name="extra"
:placeholder="$t('extra')"
v-model="extra"/>
<input
type="text"
name="distribution"
:placeholder="$t('distribution')"
v-model="distribution"/>
</div>
</template>
<script>
export default {
name: "AddressMore",
props: ['address'],
computed: {
floor: {
set(value) {
console.log('value', value);
this.address.floor = value;
},
get() {
return this.address.floor;
}
},
corridor: {
set(value) {
console.log('value', value);
this.address.corridor = value;
},
get() {
return this.address.corridor;
}
},
steps: {
set(value) {
console.log('value', value);
this.address.steps = value;
},
get() {
return this.address.steps;
}
},
flat: {
set(value) {
console.log('value', value);
this.address.flat = value;
},
get() {
return this.address.flat;
}
},
buildingName: {
set(value) {
console.log('value', value);
this.address.buildingName = value;
},
get() {
return this.address.buildingName;
}
},
extra: {
set(value) {
console.log('value', value);
this.address.extra = value;
},
get() {
return this.address.extra;
}
},
distribution: {
set(value) {
console.log('value', value);
this.address.distribution = value;
},
get() {
return this.address.distribution;
}
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="container">
<select
v-model="selected">
<option :value="{}" disabled selected>{{ $t('select_address') }}</option>
<option
v-for="item in this.addresses"
v-bind:item="item"
v-bind:key="item.id"
v-bind:value="item">
{{ item.street }}, {{ item.streetNumber }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'AddressSelection',
props: ['address', 'updateMapCenter'],
computed: {
addresses() {
return this.address.loaded.addresses;
},
selected: {
set(value) {
console.log('selected value', value);
this.address.selected.address = value;
this.updateMapCenter(value.point);
},
get() {
return this.address.selected.address;
}
},
}
};
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="container">
<select
v-model="selected">
<option :value="{}" disabled selected>{{ $t('select_city') }}</option>
<option
v-for="item in this.cities"
v-bind:item="item"
v-bind:key="item.id"
v-bind:value="item">
{{ item.code }}-{{ item.name }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'CitySelection',
props: ['address', 'getReferenceAddresses'],
computed: {
cities() {
return this.address.loaded.cities;
},
selected: {
set(value) {
console.log('selected value', value.name);
this.address.selected.city = value;
this.getReferenceAddresses(value);
},
get() {
return this.address.selected.city;
}
},
}
};
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="container">
<select
v-model="selected">
<option :value="{}" disabled selected>{{ $t('select_country') }}</option>
<option
v-for="item in this.countries"
v-bind:item="item"
v-bind:key="item.id"
v-bind:value="item">
{{ item.name }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'CountrySelection',
props: ['address', 'getCities'],
computed: {
countries() {
return this.address.loaded.countries;
},
selected: {
set(value) {
console.log('selected value', value.name);
this.address.selected.country = value;
this.getCities(value);
},
get() {
return this.address.selected.country;
}
}
}
};
</script>

View File

@ -9,8 +9,8 @@
<button class="close sc-button grey" @click="$emit('close')"> <button class="close sc-button grey" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button> <i class="fa fa-times" aria-hidden="true"></i></button>
</div> </div>
<div class="modal-body up" style="overflow-y: unset;"> <div class="body-head">
<slot name="body-fixed"></slot> <slot name="body-head"></slot>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot name="body"></slot> <slot name="body"></slot>

View File

@ -7,11 +7,15 @@ const datetimeFormats = {
month: "numeric", month: "numeric",
day: "numeric" day: "numeric"
}, },
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: { long: {
year: "numeric", year: "numeric",
month: "short", month: "numeric",
day: "numeric", day: "numeric",
weekday: "short",
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
hour12: false hour12: false

View File

@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Search\Model;
class Result
{
private float $relevance;
/**
* mixed an arbitrary result
*/
private $result;
/**
* @param float $relevance
* @param $result
*/
public function __construct(float $relevance, $result)
{
$this->relevance = $relevance;
$this->result = $result;
}
public function getRelevance(): float
{
return $this->relevance;
}
public function getResult()
{
return $this->result;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Chill\MainBundle\Search;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use Chill\MainBundle\Search\SearchProvider;
use Symfony\Component\VarDumper\Resources\functions\dump;
/**
* ***Warning*** This is an incomplete implementation ***Warning***
*/
class SearchApi
{
private EntityManagerInterface $em;
private SearchProvider $search;
public function __construct(EntityManagerInterface $em, SearchProvider $search)
{
$this->em = $em;
$this->search = $search;
}
/**
* @return Model/Result[]
*/
public function getResults(string $query, int $offset, int $maxResult): array
{
// **warning again**: this is an incomplete implementation
$results = [];
foreach ($this->getPersons($query) as $p) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $p);
}
foreach ($this->getThirdParties($query) as $t) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $t);
}
\usort($results, function(Model\Result $a, Model\Result $b) {
return ($a->getRelevance() <=> $b->getRelevance()) * -1;
});
return $results;
}
public function countResults(string $query): int
{
return 0;
}
private function getThirdParties(string $query)
{
$thirdPartiesIds = $this->em->createQuery('SELECT t.id FROM '.ThirdParty::class.' t')
->getScalarResult();
$nbResults = rand(0, 15);
if ($nbResults === 1) {
$nbResults++;
} elseif ($nbResults === 0) {
return [];
}
$ids = \array_map(function ($e) use ($thirdPartiesIds) { return $thirdPartiesIds[$e]['id'];},
\array_rand($thirdPartiesIds, $nbResults));
$a = $this->em->getRepository(ThirdParty::class)
->findById($ids);
return $a;
}
private function getPersons(string $query)
{
$params = [
SearchInterface::SEARCH_PREVIEW_OPTION => false
];
$search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json');
$ids = \array_map(function($r) { return $r['id']; }, $search['results']);
if (count($ids) === 0) {
return [];
}
return $this->em->getRepository(Person::class)
->findById($ids)
;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
public function normalize($address, string $format = null, array $context = [])
{
$data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getBuildingName();
$data['postcode']['name'] = $address->getPostCode()->getName();
return $data;
}
public function supportsNormalization($data, string $format = null)
{
return $data instanceof Address;
}
}

View File

@ -20,15 +20,16 @@
namespace Chill\MainBundle\Serializer\Normalizer; namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class DateNormalizer implements NormalizerInterface class DateNormalizer implements NormalizerInterface, DenormalizerInterface
{ {
public function normalize($date, string $format = null, array $context = array()) public function normalize($date, string $format = null, array $context = array())
{ {
/** @var \DateTimeInterface $date */ /** @var \DateTimeInterface $date */
return [ return [
'datetime' => $date->format(\DateTimeInterface::ISO8601), 'datetime' => $date->format(\DateTimeInterface::ISO8601)
'u' => $date->getTimestamp()
]; ];
} }
@ -36,4 +37,24 @@ class DateNormalizer implements NormalizerInterface
{ {
return $data instanceof \DateTimeInterface; return $data instanceof \DateTimeInterface;
} }
public function denormalize($data, string $type, string $format = null, array $context = [])
{
switch ($type) {
case \DateTime::class:
return \DateTime::createFromFormat(\DateTimeInterface::ISO8601, $data['datetime']);
case \DateTimeInterface::class:
case \DateTimeImmutable::class:
default:
return \DateTimeImmutable::createFromFormat(\DateTimeInterface::ISO8601, $data['datetime']);
}
}
public function supportsDenormalization($data, string $type, string $format = null): bool
{
return $type === \DateTimeInterface::class ||
$type === \DateTime::class ||
$type === \DateTimeImmutable::class ||
(\is_array($data) && array_key_exists('datetime', $data));
}
} }

View File

@ -0,0 +1,71 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
* Denormalize an object given a list of supported class
*/
class DiscriminatedObjectDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
/**
* The type to set for enabling this type
*/
public const TYPE = '@multi';
/**
* Should be present in context and contains an array of
* allowed types.
*/
public const ALLOWED_TYPES = 'denormalize_multi.allowed_types';
/**
* {@inheritDoc}
*/
public function denormalize($data, string $type, string $format = null, array $context = [])
{
foreach ($context[self::ALLOWED_TYPES] as $localType) {
if ($this->denormalizer->supportsDenormalization($data, $localType, $format)) {
try {
return $this->denormalizer->denormalize($data, $localType, $format, $context); } catch (RuntimeException $e) {
$lastException = $e;
}
}
}
throw new RuntimeException(sprintf("Could not find any denormalizer for those ".
"ALLOWED_TYPES: %s", \implode(", ", $context[self::ALLOWED_TYPES])));
}
/**
* {@inheritDoc}
*/
public function supportsDenormalization($data, string $type, string $format = null, array $context = [])
{
if (self::TYPE !== $type) {
return false;
}
if (0 === count($context[self::ALLOWED_TYPES] ?? [])) {
throw new \LogicException("The context should contains a list of
allowed types");
}
foreach ($context[self::ALLOWED_TYPES] as $localType) {
if ($this->denormalizer->supportsDenormalization($data, $localType, $format)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface as SerializerMetadata;
class DoctrineExistingEntityNormalizer implements DenormalizerInterface
{
private EntityManagerInterface $em;
private ClassMetadataFactoryInterface $serializerMetadataFactory;
public function __construct(EntityManagerInterface $em, ClassMetadataFactoryInterface $serializerMetadataFactory)
{
$this->em = $em;
$this->serializerMetadataFactory = $serializerMetadataFactory;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (\array_key_exists(AbstractNormalizer::OBJECT_TO_POPULATE, $context)) {
return $context[AbstractNormalizer::OBJECT_TO_POPULATE];
}
return $this->em->getRepository($type)
->find($data['id']);
}
public function supportsDenormalization($data, string $type, string $format = null)
{
if (FALSE === \is_array($data)) {
return false;
}
if (FALSE === \array_key_exists('id', $data)) {
return false;
}
if (FALSE === $this->em->getClassMetadata($type) instanceof ClassMetadata) {
return false;
}
// does have serializer metadata, and class discriminator ?
if ($this->serializerMetadataFactory->hasMetadataFor($type)) {
$classDiscriminator = $this->serializerMetadataFactory
->getMetadataFor($type)->getClassDiscriminatorMapping();
if ($classDiscriminator) {
$typeProperty = $classDiscriminator->getTypeProperty();
// check that only 2 keys
// that the second key is property
// and that the type match the class for given type property
return count($data) === 2
&& \array_key_exists($typeProperty, $data)
&& $type === $classDiscriminator->getClassForType($data[$typeProperty]);
}
}
// we do not have any class discriminator. Check that the id is the only one key
return count($data) === 1;
}
}

View File

@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** /**
* *
* * @internal we keep this normalizer, because the property 'text' may be replace by a rendering in the future
*/ */
class UserNormalizer implements NormalizerInterface class UserNormalizer implements NormalizerInterface
{ {
@ -32,8 +32,10 @@ class UserNormalizer implements NormalizerInterface
{ {
/** @var User $user */ /** @var User $user */
return [ return [
'type' => 'user',
'id' => $user->getId(), 'id' => $user->getId(),
'username' => $user->getUsername() 'username' => $user->getUsername(),
'text' => $user->getUsername()
]; ];
} }

View File

@ -172,6 +172,7 @@ abstract class AbstractExportTest extends WebTestCase
*/ */
public function testInitiateQuery($modifiers, $acl, $data) public function testInitiateQuery($modifiers, $acl, $data)
{ {
var_dump($data);
$query = $this->getExport()->initiateQuery($modifiers, $acl, $data); $query = $this->getExport()->initiateQuery($modifiers, $acl, $data);
$this->assertTrue($query instanceof QueryBuilder || $query instanceof NativeQuery, $this->assertTrue($query instanceof QueryBuilder || $query instanceof NativeQuery,

View File

@ -0,0 +1,51 @@
<?php
namespace Chill\MainBundle\Tests\Serializer\Normalizer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Chill\MainBundle\Entity\User;
class DoctrineExistingEntityNormalizerTest extends KernelTestCase
{
protected DoctrineExistingEntityNormalizer $normalizer;
protected function setUp()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$serializerFactory = self::$container->get(ClassMetadataFactoryInterface::class);
$this->normalizer = new DoctrineExistingEntityNormalizer($em, $serializerFactory);
}
/**
* @dataProvider dataProviderUserId
*/
public function testGetMappedClass($userId)
{
$data = [ 'type' => 'user', 'id' => $userId];
$supports = $this->normalizer->supportsDenormalization($data, User::class);
$this->assertTrue($supports);
}
public function dataProviderUserId()
{
self::bootKernel();
$userIds = self::$container->get(EntityManagerInterface::class)
->getRepository(User::class)
->createQueryBuilder('u')
->select('u.id')
->setMaxResults(1)
->getQuery()
->getResult()
;
yield [ $userIds[0]['id'] ];
}
}

View File

@ -0,0 +1,59 @@
---
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Chill api"
description: "Api documentation for chill. Currently, work in progress"
servers:
- url: "/api"
description: "Your current dev server"
components:
parameters:
_format:
name: _format
in: path
required: true
schema:
type: string
enum:
- json
paths:
/1.0/search.json:
get:
summary: perform a search across multiple entities
tags:
- search
- person
- thirdparty
description: >
**Warning**: This is currently a stub (not really implemented
The search is performed across multiple entities. The entities must be listed into
`type` parameters.
The results are ordered by relevance, from the most to the lowest relevant.
parameters:
- name: q
in: query
required: true
description: the pattern to search
schema:
type: string
- name: type[]
in: query
required: true
description: the type entities amongst the search is performed
schema:
type: array
items:
type: string
enum:
- person
- thirdparty
responses:
200:
description: "OK"

View File

@ -62,5 +62,7 @@ module.exports = function(encore, entries)
buildCKEditor(encore); buildCKEditor(encore);
encore.addEntry('ckeditor5', __dirname + '/Resources/public/modules/ckeditor5/index.js'); encore.addEntry('ckeditor5', __dirname + '/Resources/public/modules/ckeditor5/index.js');
// Address
encore.addEntry('address', __dirname + '/Resources/public/vuejs/Address/index.js');
}; };

View File

@ -69,6 +69,13 @@ chill_main_search:
requirements: requirements:
_format: html|json _format: html|json
chill_main_search_global:
path: '/api/1.0/search.{_format}'
controller: Chill\MainBundle\Controller\SearchController::searchApi
format: 'json'
requirements:
_format: 'json'
chill_main_advanced_search: chill_main_advanced_search:
path: /{_locale}/search/advanced/{name} path: /{_locale}/search/advanced/{name}
controller: Chill\MainBundle\Controller\SearchController::advancedSearchAction controller: Chill\MainBundle\Controller\SearchController::advancedSearchAction

View File

@ -3,6 +3,18 @@ parameters:
services: services:
Chill\MainBundle\Serializer\Normalizer\:
resource: '../Serializer/Normalizer'
autowire: true
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Doctrine\Event\:
resource: '../Doctrine/Event/'
autowire: true
tags:
- { name: 'doctrine.event_subscriber' }
chill.main.helper.translatable_string: chill.main.helper.translatable_string:
class: Chill\MainBundle\Templating\TranslatableStringHelper class: Chill\MainBundle\Templating\TranslatableStringHelper
arguments: arguments:

View File

@ -16,6 +16,7 @@ services:
$searchProvider: '@chill_main.search_provider' $searchProvider: '@chill_main.search_provider'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface' $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
$paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory' $paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
$searchApi: '@Chill\MainBundle\Search\SearchApi'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
Chill\MainBundle\Controller\PermissionsGroupController: Chill\MainBundle\Controller\PermissionsGroupController:

View File

@ -1,3 +1,10 @@
services: services:
chill_main.search_provider: chill_main.search_provider:
class: Chill\MainBundle\Search\SearchProvider class: Chill\MainBundle\Search\SearchProvider
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$search: '@Chill\MainBundle\Search\SearchProvider'

View File

@ -1,17 +1,11 @@
--- ---
services: services:
Chill\MainBundle\Serializer\Normalizer\CenterNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Serializer\Normalizer\DateNormalizer: # note: the autowiring for serializers and normalizers is declared
tags: # into ../services.yaml
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Serializer\Normalizer\UserNormalizer: Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer:
autowire: true
tags: tags:
- { name: 'serializer.normalizer', priority: 64 } - { name: 'serializer.normalizer', priority: 8 }
Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }

View File

@ -5,13 +5,19 @@ 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;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Entity\Scope;
use Symfony\Component\Workflow\Registry;
class AccompanyingCourseApiController extends ApiController class AccompanyingCourseApiController extends ApiController
{ {
@ -19,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)
@ -46,7 +79,6 @@ class AccompanyingCourseApiController extends ApiController
break; break;
case Request::METHOD_DELETE: case Request::METHOD_DELETE:
$participation = $accompanyingPeriod->removePerson($person); $participation = $accompanyingPeriod->removePerson($person);
$participation->setEndDate(new \DateTimeImmutable('now'));
break; break;
default: default:
throw new BadRequestException("This method is not supported"); throw new BadRequestException("This method is not supported");
@ -56,12 +88,76 @@ class AccompanyingCourseApiController extends ApiController
if ($errors->count() > 0) { if ($errors->count() > 0) {
// only format accepted // only format accepted
return $this->json($errors); return $this->json($errors, 422);
} }
$this->getDoctrine()->getManager()->flush(); $this->getDoctrine()->getManager()->flush();
return $this->json($participation); return $this->json($participation, 200, [], ['groups' => [ 'read' ]]);
}
public function resourceApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('resource', $id, $request, $_format, 'resource', Resource::class);
}
public function scopeApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('scope', $id, $request, $_format, 'scope', Scope::class, [ 'groups' => [ 'read' ] ]);
}
public function commentApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('comment', $id, $request, $_format, 'comment', Comment::class);
}
public function socialIssueApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('socialissue', $id, $request, $_format, 'socialIssue', SocialIssue::class, [ 'groups' => [ 'read' ] ]);
}
public function requestorApi($id, Request $request, string $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$action = 'requestor';
$accompanyingPeriod = $this->getEntity($action, $id, $request);
// a requestor may be a person or a thirdParty
$this->checkACL($action, $request, $_format, $accompanyingPeriod);
$this->onPostCheckACL($action, $request, $_format, $accompanyingPeriod);
if (Request::METHOD_DELETE === $request->getMethod()) {
$accompanyingPeriod->setRequestor(NULL);
} elseif (Request::METHOD_POST === $request->getMethod()) {
$requestor = null;
$exceptions = [];
foreach ([Person::class, ThirdParty::class] as $class) {
try {
$requestor = $this->getSerializer()
->deserialize($request->getContent(), $class, $_format, []);
} catch (RuntimeException $e) {
$exceptions[] = $e;
}
}
if ($requestor === null) {
throw new BadRequestException('Could not find any person or requestor', 0, $exceptions[0]);
}
$accompanyingPeriod->setRequestor($requestor);
} else {
throw new BadRequestException('method not supported');
}
$errors = $this->validator->validate($accompanyingPeriod);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors, 422);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($accompanyingPeriod->getRequestor(), 200, [], ['groups' => [ 'read']]);
} }
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response

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,78 +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'
])
);
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
* *
@ -322,7 +355,14 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'actions' => [ 'actions' => [
'_entity' => [ '_entity' => [
'roles' => [ 'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PUT => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
],
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
] ]
], ],
'participation' => [ 'participation' => [
@ -336,8 +376,79 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE, Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
] ]
],
'resource' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
] ]
],
'comment' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'requestor' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'scope' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'socialissue' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'socialIssueApi',
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'confirm' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
]
],
] ]
], ],
[ [
@ -360,7 +471,28 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
] ]
], ],
] ]
],
[
'class' => \Chill\PersonBundle\Entity\SocialWork\SocialIssue::class,
'name' => 'social_work_social_issue',
'base_path' => '/api/1.0/person/social-work/social-issue',
// 'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class,
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
] ]
],
]
],
] ]
]); ]);
} }

View File

@ -22,25 +22,34 @@
namespace Chill\PersonBundle\Entity; namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin; use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource; use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* AccompanyingPeriod Class * AccompanyingPeriod Class
* *
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period") * @ORM\Table(name="chill_person_accompanying_period")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period"=AccompanyingPeriod::class
* })
*/ */
class AccompanyingPeriod class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
{ {
/** /**
* Mark an accompanying period as "occasional" * Mark an accompanying period as "occasional"
@ -80,6 +89,7 @@ class AccompanyingPeriod
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @Groups({"read"})
*/ */
private $id; private $id;
@ -87,6 +97,7 @@ class AccompanyingPeriod
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(type="date") * @ORM\Column(type="date")
* @Groups({"read", "write"})
*/ */
private $openingDate; private $openingDate;
@ -94,6 +105,7 @@ class AccompanyingPeriod
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @Groups({"read", "write"})
*/ */
private $closingDate = null; private $closingDate = null;
@ -101,6 +113,7 @@ class AccompanyingPeriod
* @var string * @var string
* *
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Groups({"read", "write"})
*/ */
private $remark = ''; private $remark = '';
@ -108,17 +121,28 @@ class AccompanyingPeriod
* @var Collection * @var Collection
* *
* @ORM\OneToMany(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Comment", * @ORM\OneToMany(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Comment",
* mappedBy="accompanyingPeriod" * mappedBy="accompanyingPeriod",
* cascade={"persist", "remove"},
* orphanRemoval=true
* ) * )
*/ */
private $comments; private $comments;
/**
* @ORM\ManyToOne(
* targetEntity=Comment::class
* )
* @Groups({"read"})
*/
private ?Comment $initialComment = null;
/** /**
* @var Collection * @var Collection
* *
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class, * @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
* mappedBy="accompanyingPeriod", * mappedBy="accompanyingPeriod",
* cascade={"persist", "refresh", "remove", "merge", "detach"}) * cascade={"persist", "refresh", "remove", "merge", "detach"})
* @Groups({"read"})
*/ */
private $participations; private $participations;
@ -128,36 +152,42 @@ class AccompanyingPeriod
* @ORM\ManyToOne( * @ORM\ManyToOne(
* targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive") * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive")
* @ORM\JoinColumn(nullable=true) * @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/ */
private $closingMotive = null; private $closingMotive = null;
/** /**
* @ORM\ManyToOne(targetEntity=User::class) * @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true) * @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/ */
private $user; private $user;
/** /**
* @ORM\ManyToOne(targetEntity=User::class) * @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true) * @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
*/ */
private $createdBy; private $createdBy;
/** /**
* @var string * @var string
* @ORM\Column(type="string", length=32, nullable=true) * @ORM\Column(type="string", length=32, nullable=true)
* @Groups({"read"})
*/ */
private $step = self::STEP_DRAFT; private $step = self::STEP_DRAFT;
/** /**
* @ORM\ManyToOne(targetEntity=Origin::class) * @ORM\ManyToOne(targetEntity=Origin::class)
* @ORM\JoinColumn(nullable=true) * @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/ */
private $origin; private $origin;
/** /**
* @var string * @var string
* @ORM\Column(type="string", nullable=true) * @ORM\Column(type="string", nullable=true)
* @Groups({"read", "write"})
*/ */
private $intensity; private $intensity;
@ -172,6 +202,7 @@ class AccompanyingPeriod
* joinColumns={@ORM\JoinColumn(name="accompanying_period_id", referencedColumnName="id")}, * joinColumns={@ORM\JoinColumn(name="accompanying_period_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="scope_id", referencedColumnName="id")} * inverseJoinColumns={@ORM\JoinColumn(name="scope_id", referencedColumnName="id")}
* ) * )
* @Groups({"read"})
*/ */
private $scopes; private $scopes;
@ -189,19 +220,22 @@ class AccompanyingPeriod
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/ */
private $requestorAnonymous = false; private $requestorAnonymous = false;
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/ */
private $emergency = false; private $emergency = false;
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/ */
private $confidential = false; private $confidential = false;
@ -210,21 +244,54 @@ class AccompanyingPeriod
* *
* @ORM\OneToMany( * @ORM\OneToMany(
* targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Resource", * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Resource",
* mappedBy="accompanyingPeriod" * mappedBy="accompanyingPeriod",
* cascade={"persist", "remove"},
* orphanRemoval=true
* ) * )
* @Groups({"read"})
*/ */
private $resources; private $resources;
/**
* @ORM\ManyToMany(
* targetEntity=SocialIssue::class
* )
* @ORM\JoinTable(
* name="chill_person_accompanying_period_social_issues"
* )
* @Groups({"read"})
*/
private Collection $socialIssues;
/**
* @ORM\Column(type="datetime", nullable=true, options={"default": NULL})
*/
private \DateTimeInterface $createdAt;
/**
* @ORM\ManyToOne(
* targetEntity=User::class
* )
*/
private User $updatedBy;
/**
* @ORM\Column(type="datetime", nullable=true, options={"default": NULL})
*/
private \DateTimeInterface $updatedAt;
/** /**
* AccompanyingPeriod constructor. * AccompanyingPeriod constructor.
* *
* @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->comments = new ArrayCollection();
} }
/** /**
@ -318,23 +385,55 @@ class AccompanyingPeriod
return $this->remark; return $this->remark;
} }
/**
* @Groups({"read"})
*/
public function getComments(): Collection public function getComments(): Collection
{ {
return $this->comments; return $this->comments->filter(function (Comment $c) {
return $c !== $this->initialComment;
});
} }
public function addComment(Comment $comment): self public function addComment(Comment $comment): self
{ {
$this->comments[] = $comment; $this->comments[] = $comment;
$comment->setAccompanyingPeriod($this);
return $this; return $this;
} }
public function removeComment(Comment $comment): void public function removeComment(Comment $comment): void
{ {
$comment->setAccompanyingPeriod(null);
$this->comments->removeElement($comment); $this->comments->removeElement($comment);
} }
/**
* @Groups({"write"})
*/
public function setInitialComment(?Comment $comment = null): self
{
if (NULL !== $this->initialComment) {
$this->removeComment($this->initialComment);
}
if ($comment instanceof Comment) {
$this->addComment($comment);
}
$this->initialComment = $comment;
return $this;
}
/**
* @Groups({"read"})
*/
public function getInitialComment(): ?Comment
{
return $this->initialComment;
}
/** /**
* Get Participations Collection * Get Participations Collection
*/ */
@ -515,9 +614,9 @@ class AccompanyingPeriod
return $this->requestorPerson; return $this->requestorPerson;
} }
public function setRequestorPerson(Person $requestorPerson): self private function setRequestorPerson(Person $requestorPerson = null): self
{ {
$this->requestorPerson = ($this->requestorThirdParty === null) ? $requestorPerson : null; $this->requestorPerson = $requestorPerson;
return $this; return $this;
} }
@ -527,21 +626,53 @@ class AccompanyingPeriod
return $this->requestorThirdParty; return $this->requestorThirdParty;
} }
public function setRequestorThirdParty(ThirdParty $requestorThirdParty): self private function setRequestorThirdParty(ThirdParty $requestorThirdParty = null): self
{ {
$this->requestorThirdParty = ($this->requestorPerson === null) ? $requestorThirdParty : null; $this->requestorThirdParty = $requestorThirdParty;
return $this; return $this;
} }
/** /**
* @return Person|ThirdParty * @return Person|ThirdParty
* @Groups({"read"})
*/ */
public function getRequestor() public function getRequestor()
{ {
return $this->requestorPerson ?? $this->requestorThirdParty; return $this->requestorPerson ?? $this->requestorThirdParty;
} }
/**
* Set a requestor
*
* The requestor is either an instance of ThirdParty, or an
* instance of Person
*
* @param $requestor Person|ThirdParty
* @return self
* @throw UnexpectedValueException if the requestor is not a Person or ThirdParty
* @Groups({"write"})
*/
public function setRequestor($requestor): self
{
if ($requestor instanceof Person) {
$this->setRequestorThirdParty(NULL);
$this->setRequestorPerson($requestor);
} elseif ($requestor instanceof ThirdParty) {
$this->setRequestorThirdParty($requestor);
$this->setRequestorPerson(NULL);
} elseif (NULL === $requestor) {
$this->setRequestorPerson(NULL);
$this->setRequestorThirdParty(NULL);
} else {
throw new \UnexpectedValueException("requestor is not an instance of Person or ThirdParty");
}
return $this;
}
public function isRequestorAnonymous(): bool public function isRequestorAnonymous(): bool
{ {
return $this->requestorAnonymous; return $this->requestorAnonymous;
@ -638,6 +769,7 @@ class AccompanyingPeriod
public function addResource(Resource $resource): self public function addResource(Resource $resource): self
{ {
$resource->setAccompanyingPeriod($this);
$this->resources[] = $resource; $this->resources[] = $resource;
return $this; return $this;
@ -645,9 +777,27 @@ class AccompanyingPeriod
public function removeResource(Resource $resource): void public function removeResource(Resource $resource): void
{ {
$resource->setAccompanyingPeriod(null);
$this->resources->removeElement($resource); $this->resources->removeElement($resource);
} }
public function getSocialIssues(): Collection
{
return $this->socialIssues;
}
public function addSocialIssue(SocialIssue $socialIssue): self
{
$this->socialIssues[] = $socialIssue;
return $this;
}
public function removeSocialIssue(SocialIssue $socialIssue): void
{
$this->socialIssues->removeElement($socialIssue);
}
/** /**
* Get a list of all persons which are participating to this course * Get a list of all persons which are participating to this course
*/ */
@ -659,4 +809,25 @@ class AccompanyingPeriod
} }
); );
} }
public function setCreatedAt(\DateTimeInterface $datetime): self
{
$this->createdAt = $datetime;
return $this;
}
public function setUpdatedBy(User $user): self
{
$this->updatedBy = $user;
return $this;
}
public function setUpdatedAt(\DateTimeInterface $datetime): self
{
$this->updatedAt = $datetime;
return $this;
}
} }

View File

@ -25,17 +25,25 @@ namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
/** /**
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period_comment") * @ORM\Table(name="chill_person_accompanying_period_comment")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period_comment"=Comment::class
* })
*/ */
class Comment class Comment implements TrackCreationInterface, TrackUpdateInterface
{ {
/** /**
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Groups({"read"})
*/ */
private $id; private $id;
@ -50,27 +58,32 @@ class Comment
/** /**
* @ORM\ManyToOne(targetEntity=User::class) * @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false) * @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/ */
private $creator; private $creator;
/** /**
* @ORM\Column(type="datetime") * @ORM\Column(type="datetime")
* @Groups({"read"})
*/ */
private $createdAt; private $createdAt;
/** /**
* @ORM\Column(type="datetime") * @ORM\Column(type="datetime")
* @Groups({"read"})
*/ */
private $updatedAt; private $updatedAt;
/** /**
* @ORM\ManyToOne(targetEntity=User::class) * @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false) * @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/ */
private $updatedBy; private $updatedBy;
/** /**
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Groups({"read", "write"})
*/ */
private $content; private $content;
@ -103,6 +116,11 @@ class Comment
return $this; return $this;
} }
public function setCreatedBy(User $user): self
{
return $this->setCreator($user);
}
public function getCreatedAt(): ?\DateTimeInterface public function getCreatedAt(): ?\DateTimeInterface
{ {
return $this->createdAt; return $this->createdAt;
@ -132,7 +150,7 @@ class Comment
return $this->updatedBy; return $this->updatedBy;
} }
public function setUpdatedBy(?User $updatedBy): self public function setUpdatedBy(User $updatedBy): self
{ {
$this->updatedBy = $updatedBy; $this->updatedBy = $updatedBy;

View File

@ -23,6 +23,7 @@
namespace Chill\PersonBundle\Entity\AccompanyingPeriod; namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @ORM\Entity * @ORM\Entity
@ -34,16 +35,19 @@ class Origin
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Groups({"read"})
*/ */
private $id; private $id;
/** /**
* @ORM\Column(type="json") * @ORM\Column(type="json")
* @Groups({"read"})
*/ */
private $label; private $label;
/** /**
* @ORM\Column(type="date_immutable", nullable=true) * @ORM\Column(type="date_immutable", nullable=true)
* @Groups({"read"})
*/ */
private $noActiveAfter; private $noActiveAfter;

View File

@ -25,12 +25,18 @@ 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\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={
* "accompanying_period_resource"=Resource::class
* })
*/ */
class Resource class Resource
{ {
@ -38,6 +44,7 @@ class Resource
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Groups({"read"})
*/ */
private $id; private $id;
@ -90,7 +97,7 @@ class Resource
return $this->thirdParty; return $this->thirdParty;
} }
public function setThirdParty(?ThirdParty $thirdParty): self private function setThirdParty(?ThirdParty $thirdParty): self
{ {
$this->thirdParty = $thirdParty; $this->thirdParty = $thirdParty;
@ -102,7 +109,7 @@ class Resource
return $this->person; return $this->person;
} }
public function setPerson(?Person $person): self private function setPerson(?Person $person): self
{ {
$this->person = $person; $this->person = $person;
@ -122,7 +129,33 @@ class Resource
} }
/** /**
* @return Person|ThirdParty *
* @param $resource Person|ThirdParty
*/
public function setResource($resource): self
{
if ($resource instanceof ThirdParty) {
$this->setThirdParty($resource);
$this->setPerson(NULL);
} elseif ($resource instanceof Person) {
$this->setPerson($resource);
$this->setThirdParty(NULL);
} elseif (NULL === $resource) {
$this->setPerson(NULL);
$this->setThirdParty(NULL);
} else {
throw new \UnexpectedValueException(sprintf("the resource ".
"should be an instance of %s or %s", Person::class,
ThirdParty::class));
}
return $this;
}
/**
* @return ThirdParty|Person
* @Groups({"read", "write"})
*/ */
public function getResource() public function getResource()
{ {

View File

@ -25,6 +25,8 @@ namespace Chill\PersonBundle\Entity;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* AccompanyingPeriodParticipation Class * AccompanyingPeriodParticipation Class
@ -32,6 +34,9 @@ use Doctrine\ORM\Mapping as ORM;
* @package Chill\PersonBundle\Entity * @package Chill\PersonBundle\Entity
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period_participation") * @ORM\Table(name="chill_person_accompanying_period_participation")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period_participation"=AccompanyingPeriodParticipation::class
* })
*/ */
class AccompanyingPeriodParticipation class AccompanyingPeriodParticipation
{ {
@ -39,12 +44,14 @@ class AccompanyingPeriodParticipation
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Groups({"read"})
*/ */
private $id; private $id;
/** /**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="accompanyingPeriodParticipations") * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="accompanyingPeriodParticipations")
* @ORM\JoinColumn(name="person_id", referencedColumnName="id", nullable=false) * @ORM\JoinColumn(name="person_id", referencedColumnName="id", nullable=false)
* @Groups({"read"})
*/ */
private $person; private $person;
@ -56,11 +63,13 @@ class AccompanyingPeriodParticipation
/** /**
* @ORM\Column(type="date", nullable=false) * @ORM\Column(type="date", nullable=false)
* @Groups({"read"})
*/ */
private $startDate; private $startDate;
/** /**
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @Groups({"read"})
*/ */
private $endDate = null; private $endDate = null;

View File

@ -34,6 +34,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* Person Class * Person Class
@ -45,6 +46,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* columns={"firstName", "lastName"} * columns={"firstName", "lastName"}
* )}) * )})
* @ORM\HasLifecycleCallbacks() * @ORM\HasLifecycleCallbacks()
* @DiscriminatorMap(typeProperty="type", mapping={
* "person"=Person::class
* })
*/ */
class Person implements HasCenterInterface class Person implements HasCenterInterface
{ {

View File

@ -4,10 +4,15 @@ namespace Chill\PersonBundle\Entity\SocialWork;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/** /**
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="chill_person_social_issue") * @ORM\Table(name="chill_person_social_issue")
* @DiscriminatorMap(typeProperty="type", mapping={
* "social_issue"=SocialIssue::class
* })
*/ */
class SocialIssue class SocialIssue
{ {
@ -35,6 +40,7 @@ class SocialIssue
/** /**
* @ORM\Column(type="json") * @ORM\Column(type="json")
* @Groups({"read"})
*/ */
private $title = []; private $title = [];
@ -59,6 +65,11 @@ class SocialIssue
return $this->parent; return $this->parent;
} }
public function hasParent(): bool
{
return $this->parent !== null;
}
public function setParent(?self $parent): self public function setParent(?self $parent): self
{ {
$this->parent = $parent; $this->parent = $parent;

View File

@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Menu; namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -32,24 +33,31 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
public function buildMenu($menuId, MenuItem $menu, array $parameters): void public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{ {
$period = $parameters['accompanyingCourse'];
$menu->addChild($this->translator->trans('Resume Accompanying Course'), [ $menu->addChild($this->translator->trans('Resume Accompanying Course'), [
'route' => 'chill_person_accompanying_course_index', 'route' => 'chill_person_accompanying_course_index',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $parameters['accompanyingCourse']->getId() 'accompanying_period_id' => $period->getId()
]]) ]])
->setExtras(['order' => 10]); ->setExtras(['order' => 10]);
$menu->addChild($this->translator->trans('Edit Accompanying Course'), [ $menu->addChild($this->translator->trans('Edit Accompanying Course'), [
'route' => 'chill_person_accompanying_course_show', 'route' => 'chill_person_accompanying_course_show',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $parameters['accompanyingCourse']->getId() 'accompanying_period_id' => $period->getId()
]]) ]])
->setExtras(['order' => 20]); ->setExtras(['order' => 20]);
if (AccompanyingPeriod::STEP_DRAFT === $period->getStep()) {
// no more menu items if the period is draft
return;
}
$menu->addChild($this->translator->trans('Accompanying Course Details'), [ $menu->addChild($this->translator->trans('Accompanying Course Details'), [
'route' => 'chill_person_accompanying_course_history', 'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $parameters['accompanyingCourse']->getId() 'accompanying_period_id' => $period->getId()
]]) ]])
->setExtras(['order' => 30]); ->setExtras(['order' => 30]);
} }

View File

@ -71,6 +71,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'icons' => [ 'plus' ] 'icons' => [ 'plus' ]
]); ]);
} }
$menu->addChild($this->translator->trans('Create an accompanying course'), [
'route' => 'chill_person_accompanying_course_new'
])
->setExtras([
'order' => 11,
'icons' => [ 'plus' ]
]);
} }
/** /**

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

@ -2,6 +2,12 @@
<accompanying-course></accompanying-course> <accompanying-course></accompanying-course>
<persons-associated></persons-associated> <persons-associated></persons-associated>
<requestor></requestor> <requestor></requestor>
<social-issue></social-issue>
<referrer></referrer>
<resources></resources>
<comment></comment>
<confirm></confirm>
<!--test></test-->
</template> </template>
<script> <script>
@ -10,13 +16,25 @@ import { mapState } from 'vuex'
import AccompanyingCourse from './components/AccompanyingCourse.vue'; import AccompanyingCourse from './components/AccompanyingCourse.vue';
import PersonsAssociated from './components/PersonsAssociated.vue'; import PersonsAssociated from './components/PersonsAssociated.vue';
import Requestor from './components/Requestor.vue'; import Requestor from './components/Requestor.vue';
import SocialIssue from './components/SocialIssue.vue';
import Referrer from './components/Referrer.vue';
import Resources from './components/Resources.vue';
import Comment from './components/Comment.vue';
import Confirm from './components/Confirm.vue';
//import Test from './components/Test.vue';
export default { export default {
name: 'App', name: 'App',
components: { components: {
AccompanyingCourse, AccompanyingCourse,
PersonsAssociated, PersonsAssociated,
Requestor Requestor,
SocialIssue,
Referrer,
Resources,
Comment,
Confirm,
//Test
}, },
computed: mapState([ computed: mapState([
'accompanyingCourse' 'accompanyingCourse'

View File

@ -1,18 +1,11 @@
const
locale = 'fr',
format = 'json'
, accompanying_period_id = window.accompanyingCourseId //tmp
;
/* /*
* Endpoint chill_person_accompanying_course_api_show * Endpoint v.2 chill_api_single_accompanying_course__entity
* method GET, get AccompanyingCourse Object * method GET/HEAD, get AccompanyingCourse Instance
* *
* @accompanying_period_id___ integer * @id integer - id of accompanyingCourse
* @TODO var is not used but necessary in method signature
*/ */
let getAccompanyingCourse = (accompanying_period_id___) => { //tmp const getAccompanyingCourse = (id) => {
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/show.${format}`; const url = `/api/1.0/person/accompanying-course/${id}.json`;
return fetch(url) return fetch(url)
.then(response => { .then(response => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
@ -21,21 +14,104 @@ let getAccompanyingCourse = (accompanying_period_id___) => { //tmp
}; };
/* /*
* Endpoint chill_person_accompanying_course_api_add_participation, * Endpoint v.2 chill_api_single_accompanying_course__entity
* method PATCH, patch AccompanyingCourse Instance
*
* @id integer - id of accompanyingCourse
* @body Object - dictionary with changes to post
*/
const patchAccompanyingCourse = (id, body) => {
console.log('body', body);
const url = `/api/1.0/person/accompanying-course/${id}.json`;
return fetch(url, {
method: 'PATCH',
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');
});
};
/*
* Endpoint v.2 chill_api_single_accompanying_course_participation,
* method POST/DELETE, add/close a participation to the accompanyingCourse * method POST/DELETE, add/close a participation to the accompanyingCourse
* *
* @accompanying_period_id integer - id of accompanyingCourse * @id integer - id of accompanyingCourse
* @person_id integer - id of person * @payload integer - id of person
* @method string - POST or DELETE * @method string - POST or DELETE
*/ */
let postParticipation = (accompanying_period_id, person_id, method) => { const postParticipation = (id, payload, method) => {
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/participation.${format}` const body = { type: payload.type, id: payload.id };
const url = `/api/1.0/person/accompanying-course/${id}/participation.json`;
return fetch(url, { return fetch(url, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8' 'Content-Type': 'application/json;charset=utf-8'
}, },
body: JSON.stringify({id: person_id}) body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint v.2 chill_api_single_accompanying_course_requestor,
* method POST/DELETE, add/close a requestor to the accompanyingCourse
*
* @id integer - id of accompanyingCourse
* @payload object of type person|thirdparty
* @method string - POST or DELETE
*/
const postRequestor = (id, payload, method) => {
//console.log('payload', payload);
const body = (payload)? { type: payload.type, id: payload.id } : {};
console.log('body', body);
const url = `/api/1.0/person/accompanying-course/${id}/requestor.json`;
return fetch(url, {
method: method,
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');
});
};
/*
* Endpoint v.2 chill_api_single_accompanying_course_resource,
* method POST/DELETE, add/remove a resource to the accompanyingCourse
*
* @id integer - id of accompanyingCourse
* @payload object of type person|thirdparty
* @method string - POST or DELETE
*/
const postResource = (id, payload, method) => {
//console.log('payload', payload);
const body = { type: "accompanying_period_resource" };
switch (method) {
case 'DELETE':
body['id'] = payload.id;
break;
default:
body['resource'] = { type: payload.type, id: payload.id };
}
console.log('body', body);
const url = `/api/1.0/person/accompanying-course/${id}/resource.json`;
return fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}) })
.then(response => { .then(response => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
@ -45,5 +121,8 @@ let postParticipation = (accompanying_period_id, person_id, method) => {
export { export {
getAccompanyingCourse, getAccompanyingCourse,
postParticipation patchAccompanyingCourse,
postParticipation,
postRequestor,
postResource,
}; };

View File

@ -1,24 +1,37 @@
<template> <template>
<div class="vue-component"> <div class="vue-component" style="display: none;">
<h3>{{ $t('course.title') }}</h3> <h3>{{ $t('course.title') }}</h3>
<dl> <dl>
<dt>{{ $t('course.id') }}</dt> <dt>{{ $t('course.id') }}</dt>
<dd>{{ accompanyingCourse.id }}</dd> <dd>{{ accompanyingCourse.id }}</dd>
<dt>{{ $t('course.opening_date') }}</dt>
<dd>{{ $d(accompanyingCourse.openingDate.datetime, 'short') }}</dd>
<dt>{{ $t('course.closing_date') }}</dt>
<dd>{{ $d(accompanyingCourse.closingDate.datetime, 'short') }}</dd>
<dt>{{ $t('course.remark') }}</dt>
<dd>{{ accompanyingCourse.remark }}</dd>
<dt>{{ $t('course.closing_motive') }}</dt>
<dd>{{ accompanyingCourse.closing_motive }}</dd>
</dl> </dl>
</div> </div>
<teleport to="#header-accompanying_course-name .grid-4">
<toggle-flags></toggle-flags>
</teleport>
<teleport to="#header-accompanying_course-name .grid-3">
<p style="text-align: right;">
<span v-if="accompanyingCourse.openingDate">
<i>{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}</i>
</span><br>
<span v-if="accompanyingCourse.user">
{{ $t('course.by') }}<b>{{ accompanyingCourse.user.username }}</b>
</span>
</p>
</teleport>
</template> </template>
<script> <script>
import ToggleFlags from './ToggleFlags';
export default { export default {
name: 'AccompanyingCourse', name: 'AccompanyingCourse',
components: {
ToggleFlags
},
computed: { computed: {
accompanyingCourse() { accompanyingCourse() {
return this.$store.state.accompanyingCourse return this.$store.state.accompanyingCourse

View File

@ -0,0 +1,90 @@
<template>
<div class="vue-component">
<h3>{{ $t('comment.title') }}</h3>
<div class="error flash_message" v-if="errors.length > 0">
{{ errors[0] }}
</div>
<div v-if="initialComment">
créé par {{ initialComment.creator.text }}
le {{ $d(initialComment.createdAt.datetime, 'long') }}
<div v-if="initialComment.updatedAt.datetime !== initialComment.createdAt.datetime">
modifié par {{ initialComment.updatedBy.text }}
le {{ $d(initialComment.updatedAt.datetime, 'long') }}
</div>
</div>
<form @submit.prevent="submitform">
<textarea
name="content"
v-bind:placeholder="$t('comment.content')"
rows="8"
cols="80"
ckeditor="ckeditor"
v-model="content">
</textarea>
<ul class="record_actions">
<li>
<button type="submit" class="sc-button bt-save">{{ $t('action.save') }}</button>
</li>
<li id="xyz">
</li>
</ul>
</form>
<button v-if="initialComment !== null"
class="sc-button bt-delete"
@click="removeComment">
{{ $t('action.delete') }}
</button>
</div>
</template>
<script>
export default {
name: "Comment",
data() {
return {
formdata: {
type: "accompanying_period_comment",
content: ''
}
}
},
computed: {
initialComment() {
return this.$store.state.accompanyingCourse.initialComment;
},
content: {
set(value) {
this.formdata.content = value;
},
get() {
return (this.initialComment)? this.initialComment.content : null;
}
},
errors() {
return this.$store.state.errorMsg;
}
},
methods: {
submitform() {
console.log('submit');
this.$store.dispatch('postFirstComment', this.formdata);
},
removeComment() {
console.log('remove');
this.$store.dispatch('postFirstComment', null);
}
}
}
/*
* TODO
* - patch endpoint to update Content
* - delete/reset button ?
* - manage flash messages => specific component ?
* - ckeditor
*/
</script>

View File

@ -0,0 +1,51 @@
<template>
<div class="vue-component">
<h3>
{{ $t('confirm.title') }}
<span v-if="accompanyingCourse.step !== 'DRAFT'"
class="badge badge-pill badge-primary">
{{ $t('course.step.active') }}
</span>
<span v-else class="badge badge-pill badge-secondary">
{{ $t('course.step.draft') }}
</span>
</h3>
<p v-if="accompanyingCourse.step === 'DRAFT'">
{{ $t('confirm.text_draft') }}
</p>
<dl v-if="accompanyingCourse.closingDate">
<dt>{{ $t('course.closing_date') }}</dt>
<dd>{{ $d(accompanyingCourse.closingDate.datetime, 'short') }}</dd>
<dt>{{ $t('course.closing_motive') }}</dt>
<dd v-if="accompanyingCourse.closingMotive">{{ accompanyingCourse.closingMotive.name.fr }}</dd>
</dl>
<ul class="record_actions">
<li>
<button class="sc-button bt-save"
@click="confirmCourse">
{{ $t('confirm.ok') }}
</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "Confirm",
computed: {
accompanyingCourse() {
return this.$store.state.accompanyingCourse
}
},
methods: {
confirmCourse() {
console.log('confirmCourse');
}
}
}
</script>

View File

@ -29,10 +29,12 @@
</button> </button>
</li--> </li-->
<li> <li>
<button class="sc-button bt-remove" <button v-if="!participation.endDate"
class="sc-button bt-remove"
:title="$t('action.remove')" :title="$t('action.remove')"
@click.prevent="$emit('close', participation)"> @click.prevent="$emit('close', participation)">
</button> </button>
<button v-else class="sc-button bt-remove disabled"></button>
</li> </li>
</ul> </ul>
</td> </td>

View File

@ -22,14 +22,16 @@
</person-item> </person-item>
</tbody> </tbody>
</table> </table>
<add-persons></add-persons>
<ul class="record_actions"> <add-persons
<!--li> buttonTitle="persons_associated.add_persons"
<button class="sc-button orange" @click="savePersons"> modalTitle="add_persons.title"
{{ $t('action.save') }} v-bind:key="addPersons.key"
</button> v-bind:options="addPersons.options"
</li--> @addNewPersons="addNewPersons"
</ul> ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div> </div>
</template> </template>
@ -44,23 +46,40 @@ export default {
PersonItem, PersonItem,
AddPersons AddPersons
}, },
data() {
return {
addPersons: {
key: 'persons_associated',
options: {
type: ['person'],
priority: null,
uniq: false,
}
}
}
},
computed: mapState({ computed: mapState({
participations: state => state.accompanyingCourse.participations, participations: state => state.accompanyingCourse.participations,
counter: state => state.accompanyingCourse.participations.length counter: state => state.accompanyingCourse.participations.length
}), }),
methods: { methods: {
removeParticipation(item) { removeParticipation(item) {
this.$store.dispatch('removeParticipation', item) console.log('@@ CLICK remove participation: item', item);
this.$store.dispatch('removeParticipation', item);
}, },
closeParticipation(item) { closeParticipation(item) {
console.log('@@ CLICK close participation: item', item); console.log('@@ CLICK close participation: item', item);
this.$store.dispatch('closeParticipation', item) this.$store.dispatch('closeParticipation', item);
}, },
/* addNewPersons({ selected, modal }) {
savePersons() { console.log('@@@ CLICK button addNewPersons', selected);
console.log('[wip] saving persons'); selected.forEach(function(item) {
this.$store.dispatch('addParticipation', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
} }
*/
} }
} }
</script> </script>

View File

@ -0,0 +1,11 @@
<template>
<div class="vue-component">
<h3>{{ $t('referrer.title') }}</h3>
</div>
</template>
<script>
export default {
name: "Referrer",
}
</script>

View File

@ -1,85 +1,112 @@
<template> <template>
<div class="vue-component"> <div class="vue-component">
<h3>{{ $t('requestor.title') }}</h3> <h3>{{ $t('requestor.title') }}</h3>
{{ accompanyingCourse.id }}
{{ accompanyingCourse.remark }}<br><br>
<!-- TESTS AREA --> <div v-if="accompanyingCourse.requestor">
<ul class="record_actions"> <label>
<li> <input type="checkbox" v-model="isAnonymous" :value="value" />
<button class="sc-button bt-create" @click="modal1.showModal = true"> {{ $t('requestor.is_anonymous') }}
{{ $t('action.show_modal') }} </label>
<dt>{{ $t('requestor.type') }}</dt>
<dd>{{ accompanyingCourse.requestor.type }}</dd>
<dt>{{ $t('requestor.text') }}</dt>
<dd>{{ accompanyingCourse.requestor.text }}</dd>
<dt>{{ $t('requestor.is_anonymous') }}</dt>
<dd>{{ accompanyingCourse.requestorAnonymous }}</dd>
<div v-if="accompanyingCourse.requestor.type === 'person'">
<dt>{{ $t('requestor.person_id') }}</dt>
<dd>{{ accompanyingCourse.requestor.person_id }}</dd>
<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.firstName') }}</dt>
<dd>{{ accompanyingCourse.requestor.firstName }}</dd>
<dt>{{ $t('requestor.lastName') }}</dt>
<dd>{{ accompanyingCourse.requestor.lastName }}</dd>
<dt>{{ $t('requestor.phonenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.phonenumber }}</dd>
<dt>{{ $t('requestor.mobilenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.mobilenumber }}</dd>
<dt>{{ $t('requestor.altNames') }}</dt>
<dd>{{ accompanyingCourse.requestor.altNames }}</dd>
</div>
<div v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<dt>{{ $t('requestor.person_id') }}</dt>
<dd>{{ accompanyingCourse.requestor.thirdparty_id }}</dd>
<dt>{{ $t('requestor.address') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.text }}</dd>
<dt>{{ $t('requestor.location') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.postcode.name }}</dd>
</div>
</div>
<button v-if="accompanyingCourse.requestor !== null"
class="sc-button bt-remove"
:title="$t('action.remove')"
@click="removeRequestor">
</button> </button>
</li>
<li>
<button class="sc-button bt-create" @click="modal2.showModal = true">
Ouvrir une seconde modale
</button>
</li>
</ul>
<teleport to="body"> <add-persons v-if="accompanyingCourse.requestor === null"
<modal v-if="modal1.showModal" :modalDialogClass="modal1.modalDialogClass" @close="modal1.showModal = false"> buttonTitle="requestor.add_requestor"
<template v-slot:header> modalTitle="requestor.add_requestor"
<h3 class="modal-title">Le titre de ma modale</h3> v-bind:key="addPersons.key"
</template> v-bind:options="addPersons.options"
<template v-slot:body> @addNewPersons="addNewPersons"
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p> ref="addPersons"> <!-- to cast child method -->
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p> </add-persons>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal1.showModal = false; modal2.showModal = true">
{{ $t('action.next')}}</button>
</template>
</modal>
</teleport>
<teleport to="body">
<modal v-if="modal2.showModal" :modalDialogClass="modal2.modalDialogClass" @close="modal2.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Une autre modale</h3>
</template>
<template v-slot:body>
<p>modal 2</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal2.showModal = false">
{{ $t('action.save')}}</button>
</template>
</modal>
</teleport>
<!-- END TESTS -->
</div> </div>
</template> </template>
<script> <script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal' import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
export default { export default {
name: 'Requestor', name: 'Requestor',
components: { components: {
Modal, AddPersons,
}, },
data() { data() {
return { return {
modal1: { addPersons: {
showModal: false, key: 'requestor',
modalDialogClass: "modal-dialog-scrollable modal-xl" // modal-lg modal-md modal-sm options: {
}, type: ['person', 'thirdparty'],
modal2: { priority: null,
showModal: false, uniq: true,
modalDialogClass: "modal-dialog-centered modal-sm" // modal-lg modal-md modal-sm }
} }
} }
}, },
computed: { computed: {
accompanyingCourse() { accompanyingCourse() {
return this.$store.state.accompanyingCourse return this.$store.state.accompanyingCourse
},
isAnonymous: {
set(value) {
console.log('requestorIsAnonymous value',value);
this.$store.dispatch('requestorIsAnonymous', value);
},
get() {
return this.$store.state.accompanyingCourse.requestorAnonymous;
}
}
},
methods: {
removeRequestor() {
console.log('@@ CLICK remove requestor: item');
this.$store.dispatch('removeRequestor');
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
this.$store.dispatch('addRequestor', selected.shift());
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
} }
} }
} }

View File

@ -0,0 +1,68 @@
<template>
<tr>
<td>
<span class="badge badge-pill badge-secondary"
v-bind:title="resource.resource.id">
<span v-if="resource.resource.type === 'person'" >{{ $t('item.type_person') }}</span>
<span v-if="resource.resource.type === 'thirdparty'" >{{ $t('item.type_thirdparty') }}</span>
</span>
{{ resource.resource.text }}
</td>
<td v-if="resource.resource.type === 'person'">
{{ $tc('person.born') }}{{ $d(resource.resource.birthdate.datetime, 'short') }}
</td>
<td v-else-if="resource.resource.type === 'thirdparty'">
{{ resource.resource.address.text }}<br>
{{ resource.resource.address.postcode.name }}
</td>
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
</li>
<li>
<button
class="sc-button bt-remove"
:title="$t('action.remove')"
@click.prevent="$emit('remove', resource)">
</button>
</li>
</ul>
</td>
</tr>
</template>
<script>
export default {
name: 'ResourceItem',
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`
}
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<div class="vue-component">
<h3>{{ $t('resources.title')}}</h3>
<label>{{ $tc('resources.counter', counter) }}</label>
<table class="rounded">
<thead>
<tr>
<th class="chill-orange">{{ $t('resources.text') }}</th>
<th class="chill-orange">{{ $t('resources.description') }}</th>
<th class="chill-orange">{{ $t('action.actions') }}</th>
</tr>
</thead>
<tbody>
<resource-item
v-for="resource in resources"
v-bind:resource="resource"
v-bind:key="resource.id"
@remove="removeResource">
</resource-item>
</tbody>
</table>
<add-persons
buttonTitle="resources.add_resources"
modalTitle="resources.add_resources"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
<script>
import { mapState } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import ResourceItem from './ResourceItem.vue';
export default {
name: 'Resources',
components: {
AddPersons,
ResourceItem
},
data() {
return {
addPersons: {
key: 'resources',
options: {
type: ['person', 'thirdparty'],
priority: null,
uniq: false,
}
}
}
},
computed: mapState({
resources: state => state.accompanyingCourse.resources,
counter: state => state.accompanyingCourse.resources.length
}),
methods: {
removeResource(item) {
console.log('@@ CLICK remove resource: item', item);
this.$store.dispatch('removeResource', item);
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addResource', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
}
}
}
</script>

View File

@ -0,0 +1,11 @@
<template>
<div class="vue-component">
<h3>{{ $t('social_issue.title') }}</h3>
</div>
</template>
<script>
export default {
name: "SocialIssue",
}
</script>

View File

@ -0,0 +1,81 @@
<template>
<div class="vue-component">
<h3>Tests</h3>
<!-- Modal -->
<ul class="record_actions">
<li>
<button class="sc-button bt-create" @click="modal1.showModal = true">
{{ $t('action.show_modal') }}
</button>
</li>
<li>
<button class="sc-button bt-create" @click="modal2.showModal = true">
Ouvrir une seconde modale
</button>
</li>
</ul>
<teleport to="body">
<modal v-if="modal1.showModal" :modalDialogClass="modal1.modalDialogClass" @close="modal1.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Le titre de ma modale</h3>
</template>
<template v-slot:body>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal1.showModal = false; modal2.showModal = true">
{{ $t('action.next')}}</button>
</template>
</modal>
</teleport>
<teleport to="body">
<modal v-if="modal2.showModal" :modalDialogClass="modal2.modalDialogClass" @close="modal2.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Une autre modale</h3>
</template>
<template v-slot:body>
<p>modal 2</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal2.showModal = false">
{{ $t('action.save')}}</button>
</template>
</modal>
</teleport>
<!-- END Modal -->
</div>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
export default {
name: 'Test',
components: {
Modal,
},
data() {
return {
modal1: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" // modal-lg modal-md modal-sm
},
modal2: {
showModal: false,
modalDialogClass: "modal-dialog-centered modal-sm" // modal-lg modal-md modal-sm
}
}
},
computed: {
}
}
</script>

View File

@ -0,0 +1,68 @@
<template>
<p style="text-align: right;">
<a @click="toggleIntensity" class="flag-toggle">
{{ $t('course.occasional') }}
<i class="fa" :class="{ 'fa-toggle-on': isRegular, 'fa-toggle-off': !isRegular }"></i>
{{ $t('course.regular') }}
</a>
<button class="badge badge-pill"
:class="{ 'badge-primary': isEmergency, 'badge-secondary': !isEmergency }"
@click="toggleEmergency">
{{ $t('course.emergency') }}
</button>
<button class="badge badge-pill"
:class="{ 'badge-primary': isConfidential, 'badge-secondary': !isConfidential }"
@click="toggleConfidential">
{{ $t('course.confidential') }}
</button>
</p>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: "ToggleFlags",
computed: {
...mapState({
intensity: state => state.accompanyingCourse.intensity,
emergency: state => state.accompanyingCourse.emergency,
confidential: state => state.accompanyingCourse.confidential,
}),
isRegular() {
return (this.intensity === 'regular')? true : false;
},
isEmergency() {
return (this.emergency) ? true : false;
},
isConfidential() {
return (this.confidential) ? true : false;
}
},
methods: {
toggleIntensity() {
let value;
switch (this.intensity) {
case "occasional":
value = "regular";
break;
case "regular":
value = "occasional";
break;
default:
//temporaire (modif backend)
value = "occasional";
}
this.$store.dispatch('toggleIntensity', value);
},
toggleEmergency() {
this.$store.dispatch('toggleEmergency', (!this.isEmergency));
},
toggleConfidential() {
this.$store.dispatch('toggleConfidential', (!this.isConfidential));
}
}
}
</script>

View File

@ -9,6 +9,20 @@ const appMessages = {
closing_date: "Date de clôture", closing_date: "Date de clôture",
remark: "Commentaire", remark: "Commentaire",
closing_motive: "Motif de clôture", closing_motive: "Motif de clôture",
user: "TMS",
flags: "Indicateurs",
status: "État",
step: {
draft: "Brouillon",
open: "Ouvert",
active: "En file active"
},
open_at: "ouvert le ",
by: "par ",
emergency: "urgent",
confidential: "confidentiel",
regular: "régulier",
occasional: "ponctuel"
}, },
persons_associated: { persons_associated: {
title: "Usagers concernés", title: "Usagers concernés",
@ -17,11 +31,48 @@ const appMessages = {
lastname: "Nom", lastname: "Nom",
startdate: "Date d'entrée", startdate: "Date d'entrée",
enddate: "Date de sortie", enddate: "Date de sortie",
addPerson: "Ajouter un usager", add_persons: "Ajouter des usagers",
}, },
requestor: { requestor: {
title: "Demandeur", title: "Demandeur",
add_requestor: "Ajouter un demandeur",
is_anonymous: "Le demandeur est anonyme",
type: "Type",
person_id: "id",
text: "Dénomination",
firstName: "Prénom",
lastName: "Nom",
birthdate: "Date de naissance",
center: "Centre",
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altNames: "Autres noms",
address: "Adresse",
location: "Localité",
}, },
social_issue: {
title: "Problématiques sociales",
},
referrer: {
title: "Référent",
},
resources: {
title: "Interlocuteurs privilégiés",
counter: "Pas d'interlocuteur | 1 interlocuteur | {count} interlocuteurs",
text: "Dénomination",
description: "Description",
add_resources: "Ajouter des interlocuteurs",
},
comment: {
title: "Ajout d'une note",
content: "Rédigez une première note..."
},
confirm: {
title: "Confirmation",
text_draft: "Le parcours est actuellement au statut de brouillon. En validant cette étape, vous lui donnez le statut actif.",
ok: "Activer le parcours"
},
} }
}; };

View File

@ -1,11 +1,13 @@
import 'es6-promise/auto'; import 'es6-promise/auto';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import addPersons from './modules/addPersons' import { getAccompanyingCourse,
import { getAccompanyingCourse, postParticipation } from '../api'; patchAccompanyingCourse,
postParticipation,
postRequestor,
postResource } from '../api';
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId;
const id = window.accompanyingCourseId; //tmp
let initPromise = getAccompanyingCourse(id) let initPromise = getAccompanyingCourse(id)
.then(accompanying_course => new Promise((resolve, reject) => { .then(accompanying_course => new Promise((resolve, reject) => {
@ -13,7 +15,6 @@ let initPromise = getAccompanyingCourse(id)
const store = createStore({ const store = createStore({
strict: debug, strict: debug,
modules: { modules: {
addPersons
}, },
state: { state: {
accompanyingCourse: accompanying_course, accompanyingCourse: accompanying_course,
@ -22,54 +23,154 @@ let initPromise = getAccompanyingCourse(id)
getters: { getters: {
}, },
mutations: { mutations: {
removeParticipation(state, item) { catchError(state, error) {
//console.log('mutation: remove item', item.id); state.errorMsg.push(error);
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== item); },
removeParticipation(state, participation) {
//console.log('### mutation: remove participation', participation.id);
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(element => element !== participation);
}, },
closeParticipation(state, { participation, payload }) { closeParticipation(state, { participation, payload }) {
console.log('### mutation: close item', { participation, payload }); //console.log('### mutation: close item', { participation, payload });
// trouve dans le state le payload et le supprime du state // find row position and replace by closed participation
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== payload); state.accompanyingCourse.participations.splice(
// pousse la participation state.accompanyingCourse.participations.findIndex(element => element === payload), 1, participation
state.accompanyingCourse.participations.push(participation); );
}, },
addParticipation(state, participation) { addParticipation(state, participation) {
//console.log('### mutation: add participation', participation); //console.log('### mutation: add participation', participation);
state.accompanyingCourse.participations.push(participation); state.accompanyingCourse.participations.push(participation);
}, },
removeRequestor(state) {
//console.log('### mutation: removeRequestor');
state.accompanyingCourse.requestor = null;
},
addRequestor(state, requestor) {
//console.log('### mutation: addRequestor', requestor);
state.accompanyingCourse.requestor = requestor;
},
requestorIsAnonymous(state, value) {
//console.log('### mutation: requestorIsAnonymous', value);
state.accompanyingCourse.requestorAnonymous = value;
},
removeResource(state, resource) {
//console.log('### mutation: removeResource', resource);
state.accompanyingCourse.resources = state.accompanyingCourse.resources.filter(element => element !== resource);
},
addResource(state, resource) {
//console.log('### mutation: addResource', resource);
state.accompanyingCourse.resources.push(resource);
},
toggleIntensity(state, value) {
state.accompanyingCourse.intensity = value;
},
toggleEmergency(state, value) {
//console.log('### mutation: toggleEmergency');
state.accompanyingCourse.emergency = value;
},
toggleConfidential(state, value) {
//console.log('### mutation: toggleConfidential');
state.accompanyingCourse.confidential = value;
},
postFirstComment(state, comment) {
console.log('### mutation: postFirstComment', comment);
state.accompanyingCourse.initialComment = comment;
}
}, },
actions: { actions: {
removeParticipation({ commit }, payload) { removeParticipation({ commit }, payload) {
commit('removeParticipation', payload); commit('removeParticipation', payload);
// fetch DELETE request...
}, },
closeParticipation({ commit }, payload) { closeParticipation({ commit }, payload) {
//console.log('## action: fetch delete participation: payload', payload.person.id); //console.log('## action: fetch delete participation: payload', payload);
postParticipation(id, payload.person.id, 'DELETE') postParticipation(id, payload.person, 'DELETE')
.then(participation => new Promise((resolve, reject) => { .then(participation => new Promise((resolve, reject) => {
//console.log('payload', payload);
commit('closeParticipation', { participation, payload }); commit('closeParticipation', { participation, payload });
resolve(); resolve();
})) })).catch((error) => { commit('catchError', error) });
.catch((error) => {
state.errorMsg.push(error.message);
});
}, },
addParticipation(addPersons, payload) { addParticipation({ commit }, payload) {
//console.log('## action: fetch post participation: payload', payload.id); //console.log('## action: fetch post participation (select item): payload', payload);
postParticipation(id, payload.id, 'POST') postParticipation(id, payload.result, 'POST')
.then(participation => new Promise((resolve, reject) => { .then(participation => new Promise((resolve, reject) => {
//console.log(participation, payload); commit('addParticipation', participation);
addPersons.commit('addParticipation', participation);
addPersons.commit('resetState', payload);
resolve(); resolve();
})) })).catch((error) => { commit('catchError', error) });
.catch((error) => {
state.errorMsg.push(error.message);
});
}, },
removeRequestor({ commit, dispatch }) {
//console.log('## action: fetch delete requestor');
postRequestor(id, null, 'DELETE')
.then(requestor => new Promise((resolve, reject) => {
commit('removeRequestor');
dispatch('requestorIsAnonymous', false);
resolve();
})).catch((error) => { commit('catchError', error) });
},
addRequestor({ commit }, payload) {
//console.log('## action: fetch post requestor: payload', payload);
postRequestor(id, payload.result, 'POST')
.then(requestor => new Promise((resolve, reject) => {
commit('addRequestor', requestor);
resolve();
})).catch((error) => { commit('catchError', error) });
},
requestorIsAnonymous({ commit }, payload) {
//console.log('## action: fetch patch AccompanyingCourse: payload', payload);
patchAccompanyingCourse(id, { type: "accompanying_period", requestorAnonymous: payload })
.then(course => new Promise((resolve, reject) => {
commit('requestorIsAnonymous', course.requestorAnonymous)
resolve();
})).catch((error) => { commit('catchError', error) });
},
removeResource({ commit }, payload) {
//console.log('## action: fetch postResource: payload', payload);
postResource(id, payload, 'DELETE')
.then(resource => new Promise((resolve, reject) => {
commit('removeResource', payload) // mieux un retour de l'objet !
resolve();
})).catch((error) => { commit('catchError', error) });
},
addResource({ commit }, payload) {
//console.log('## action: fetch postResource: payload', payload);
postResource(id, payload.result, 'POST')
.then(resource => new Promise((resolve, reject) => {
commit('addResource', resource)
resolve();
})).catch((error) => { commit('catchError', error) });
},
toggleIntensity({ commit }, payload) {
console.log(payload);
patchAccompanyingCourse(id, { type: "accompanying_period", intensity: payload })
.then(course => new Promise((resolve, reject) => {
commit('toggleIntensity', course.intensity);
resolve();
})).catch((error) => { commit('catchError', error) });
},
toggleEmergency({ commit }, payload) {
patchAccompanyingCourse(id, { type: "accompanying_period", emergency: payload })
.then(course => new Promise((resolve, reject) => {
commit('toggleEmergency', course.emergency);
resolve();
})).catch((error) => { commit('catchError', error) });
},
toggleConfidential({ commit }, payload) {
patchAccompanyingCourse(id, { type: "accompanying_period", confidential: payload })
.then(course => new Promise((resolve, reject) => {
commit('toggleConfidential', course.confidential);
resolve();
})).catch((error) => { commit('catchError', error) });
},
postFirstComment({ commit }, payload) {
console.log('## action: postFirstComment: payload', payload);
patchAccompanyingCourse(id, { type: "accompanying_period", initialComment: payload })
.then(course => new Promise((resolve, reject) => {
commit('postFirstComment', course.initialComment);
resolve();
})).catch((error) => { commit('catchError', error) });
}
} }
}); });
//console.log('store object', store.state.accompanyingCourse.id);
resolve(store); resolve(store);
})); }));

View File

@ -1,76 +0,0 @@
import { searchPersons } from 'ChillPersonAssets/vuejs/_api/AddPersons'
import { postParticipation } from '../../api';
// initial state
const state = {
query: "",
suggested: [],
selected: []
}
// getters
const getters = {
selectedAndSuggested: state => {
const uniqBy = (a, key) => [
...new Map(
a.map(x => [key(x), x])
).values()
];
let union = [...new Set([
...state.suggested.slice().reverse(),
...state.selected.slice().reverse(),
])];
return uniqBy(union, k => k.id);
}
}
// mutations
const mutations = {
setQuery(state, query) {
//console.log('q=', query);
state.query = query;
},
loadSuggestions(state, suggested) {
state.suggested = suggested;
},
updateSelected(state, value) {
state.selected = value;
},
resetState(state, selected) {
//console.log('avant', state.selected);
state.selected = state.selected.filter(value => value !== selected);
//console.log('après', state.selected);
state.query = "";
state.suggested = [];
}
}
// actions
const actions = {
setQuery({ commit }, payload) {
//console.log('## action: setquery: payload', payload);
commit('setQuery', payload.query);
if (payload.query.length >= 3) {
searchPersons(payload.query)
.then(suggested => new Promise((resolve, reject) => {
commit('loadSuggestions', suggested.results);
resolve();
}));
} else {
commit('loadSuggestions', []);
}
},
updateSelected({ commit }, payload) {
//console.log('## action: update selected values: payload', payload);
commit('updateSelected', payload);
}
}
export default {
//namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -1,15 +1,23 @@
const /*
locale = 'fr', * Build query string with query and options
format = 'json' */
; const parametersToString = ({ query, options }) => {
let types ='';
options.type.forEach(function(type) {
types += '&type[]=' + type;
});
return 'q=' + query + types;
};
/* /*
* Endpoint chill_person_search, method GET, get a list of persons * Endpoint chill_person_search
* method GET, get a list of persons
* *
* @query string - the query to search for * @query string - the query to search for
*/ */
let searchPersons = (query) => { const searchPersons = ({ query, options }) => {
let url = `/${locale}/search.${format}?name=person_regular&q=${query}`; let queryStr = parametersToString({ query, options });
let url = `/fr/search.json?name=person_regular&${queryStr}`;
return fetch(url) return fetch(url)
.then(response => { .then(response => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
@ -17,4 +25,22 @@ let searchPersons = (query) => {
}); });
}; };
export { searchPersons }; /*
* Endpoint v.2 chill_main_search_global
* method GET, get a list of persons and thirdparty
*
* NOTE: this is a temporary WIP endpoint, return inconsistent random results
* @query string - the query to search for
*/
const searchPersons_2 = ({ query, options }) => {
let queryStr = parametersToString({ query, options });
let url = `/api/1.0/search.json?${queryStr}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export { searchPersons, searchPersons_2 };

View File

@ -1,6 +1,6 @@
<template> <template>
<button class="sc-button bt-create centered mt-4" @click="openModal"> <button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_persons.search_add_others_persons') }} {{ $t(buttonTitle) }}
</button> </button>
<teleport to="body"> <teleport to="body">
@ -9,11 +9,13 @@
@close="modal.showModal = false"> @close="modal.showModal = false">
<template v-slot:header> <template v-slot:header>
<h3 class="modal-title">{{ $t('add_persons.title') }}</h3> <h3 class="modal-title">{{ $t(modalTitle) }}</h3>
</template> </template>
<template v-slot:body-fixed> <template v-slot:body-head>
<div class="modal-body">
<div class="search"> <div class="search">
<label style="float: right;"> <label style="float: right;">
{{ $tc('add_persons.suggested_counter', suggestedCounter) }} {{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label> </label>
@ -24,28 +26,39 @@
:placeholder="$t('add_persons.search_some_persons')" :placeholder="$t('add_persons.search_some_persons')"
ref="search" /> ref="search" />
<i class="fa fa-search fa-lg"></i> <i class="fa fa-search fa-lg"></i>
<i class="fa fa-times" v-if="queryLength >= 3" @click="resetSuggestion"></i>
</div>
</div>
<div class="modal-body" v-if="checkUniq === 'checkbox'">
<div class="count">
<span>
<a v-if="suggestedCounter > 2" @click="selectAll">
{{ $t('action.check_all')}}
</a>
<a v-if="selectedCounter > 0" @click="resetSelection">
<i v-if="suggestedCounter > 2"> </i>
{{ $t('action.reset')}}
</a>
</span>
<span v-if="selectedCounter > 0">
{{ $tc('add_persons.selected_counter', selectedCounter) }}
</span>
</div>
</div> </div>
</template> </template>
<template v-slot:body> <template v-slot:body>
<!--span class="discret">Selection: {{ selected }}</span--> <!--span class="discret">Selection: {{ selected }}</span-->
<div class="results"> <div class="results">
<div class="count">
<span>
<a v-if="suggestedCounter > 0" href="#">
{{ $t('action.check_all')}}</a>
<a v-if="selectedCounter > 0" href="#">
{{ $t('action.reset')}}</a>
</span>
<span v-if="selectedCounter > 0">
{{ $tc('add_persons.selected_counter', selectedCounter) }}
</span>
</div>
<person-suggestion <person-suggestion
v-for="item in this.selectedAndSuggested.slice().reverse()" v-for="item in this.selectedAndSuggested.slice().reverse()"
v-bind:key="itemKey(item)"
v-bind:item="item" v-bind:item="item"
v-bind:key="item.id"> v-bind:search="search"
v-bind:type="checkUniq"
@updateSelected="updateSelected">
</person-suggestion> </person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson"> <button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
@ -55,7 +68,8 @@
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<button class="sc-button green" @click="addNewPersons"> <button class="sc-button green"
@click.prevent="$emit('addNewPersons', { selected, modal })">
<i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}} <i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}}
</button> </button>
</template> </template>
@ -65,9 +79,9 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex';
import Modal from 'ChillMainAssets/vuejs/_components/Modal'; import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import PersonSuggestion from 'ChillPersonAssets/vuejs/_components/PersonSuggestion'; import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
export default { export default {
name: 'AddPersons', name: 'AddPersons',
@ -75,40 +89,69 @@ export default {
Modal, Modal,
PersonSuggestion, PersonSuggestion,
}, },
props: [
'buttonTitle',
'modalTitle',
'options'
],
emits: ['addNewPersons'],
data() { data() {
return { return {
modal: { modal: {
showModal: false, showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" modalDialogClass: "modal-dialog-scrollable modal-xl"
},
search: {
query: "",
suggested: [],
selected: []
} }
} }
}, },
computed: { computed: {
...mapState({
addPersons: state => state.addPersons
}),
query: { query: {
set(query) { set(query) {
this.$store.dispatch('setQuery', { query }); return this.setQuery(query);
}, },
get() { get() {
return this.addPersons.query; return this.search.query;
} }
}, },
queryLength() {
return this.search.query.length;
},
suggested() { suggested() {
return this.addPersons.suggested; return this.search.suggested;
}, },
suggestedCounter() { suggestedCounter() {
return this.addPersons.suggested.length; return this.search.suggested.length;
}, },
selected() { selected() {
return this.addPersons.selected; return this.search.selected;
}, },
selectedCounter() { selectedCounter() {
return this.addPersons.selected.length; return this.search.selected.length;
}, },
selectedAndSuggested() { selectedAndSuggested() {
return this.$store.getters.selectedAndSuggested; const uniqBy = (a, key) => [
...new Map(
a.map(x => [key(x), x])
).values()
];
let union = [...new Set([
...this.suggested.slice().reverse(),
...this.selected.slice().reverse(),
])];
return uniqBy(union, k => k.key);
},
options() {
return this.options;
},
checkUniq() {
if (this.options.uniq === true) {
return 'radio';
}
return 'checkbox';
} }
}, },
methods: { methods: {
@ -118,15 +161,49 @@ export default {
this.$refs.search.focus(); this.$refs.search.focus();
}) })
}, },
addNewPersons() { setQuery(query) {
console.log('@@@ CLICK button addPersons') this.search.query = query;
this.selected.forEach(function(item) { if (query.length >= 3) {
//console.log('# dispatch action for each item', item); searchPersons_2({ query, options: this.options })
this.$store.dispatch('addParticipation', item); .then(suggested => new Promise((resolve, reject) => {
}, this console.log('suggested', suggested);
); this.loadSuggestions(suggested.results);
this.modal.showModal = false; resolve();
}));
} else {
this.loadSuggestions([]);
} }
},
loadSuggestions(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);
this.search.selected = value;
},
resetSearch() {
this.resetSelection();
this.resetSuggestion();
},
resetSuggestion() {
this.search.query = "";
this.search.suggested = [];
},
resetSelection() {
this.search.selected = [];
},
selectAll() {
this.search.suggested.forEach(function(item) {
this.search.selected.push(item);
}, this);
},
itemKey(item) {
return item.result.type + item.result.id;
} }
},
} }
</script> </script>

View File

@ -0,0 +1,62 @@
<template>
<div class="list-item" :class="{ checked: isChecked }">
<div class="container">
<input
v-bind:type="type"
v-model="selected"
name="item"
v-bind:id="item"
v-bind:value="setValueIfType(item, type)" />
</div>
<suggestion-person
v-if="item.result.type === 'person'"
v-bind:item="item">
</suggestion-person>
<suggestion-third-party
v-if="item.result.type === 'thirdparty'"
v-bind:item="item">
</suggestion-third-party>
</div>
</template>
<script>
import SuggestionPerson from './TypePerson';
import SuggestionThirdParty from './TypeThirdParty';
export default {
name: 'PersonSuggestion',
components: {
SuggestionPerson,
SuggestionThirdParty,
},
props: [
'item',
'search',
'type'
],
emits: ['updateSelected'],
computed: {
selected: {
set(value) {
console.log('value', value);
this.$emit('updateSelected', value);
},
get() {
return this.search.selected;
}
},
isChecked() {
return (this.search.selected.indexOf(this.item) === -1) ? false : true;
},
},
methods: {
setValueIfType(value, type) {
return (type === 'radio')? [value] : value;
}
}
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
</span>
<span class="birthday">
{{ $d(item.result.birthdate.datetime, 'short') }}
</span>
</div>
<div class="right_actions">
<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>
</div>
</template>
<script>
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'
},
}
},
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
</span>
<span class="location">
{{ item.result.address.text }} -
{{ item.result.address.postcode.name }}
</span>
</div>
<div class="right_actions">
<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>
</div>
</template>
<script>
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'
},
}
},
}
</script>

View File

@ -1,53 +0,0 @@
<template>
<div class="list-item" :class="{ checked: isChecked }">
<div class="container">
<!--a class="discret" target="_blank" :href="url.show">{{ item.id }}</a-->
<input class=""
type="checkbox"
v-model="selected"
:value="item" />
{{ item.text }}
</div>
<div class="right_actions">
<span class="badge badge-pill badge-secondary" :title="item.id">
{{ $t('item.type_person') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.id" :href="url.show"></a>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'PersonSuggestion',
props: ['item'],
data() {
return {
url: {
show: '/fr/person/' + this.item.id + '/general',
edit: '/fr/person/' + this.item.id + '/general/edit'
}
}
},
computed: {
selected: {
set(value) {
this.$store.dispatch('updateSelected', value);
},
get() {
return this.$store.state.addPersons.selected;
}
},
isChecked() {
return (this.selected.indexOf(this.item) === -1) ? false : true;
}
}
};
</script>

View File

@ -1,7 +1,6 @@
const personMessages = { const personMessages = {
fr: { fr: {
add_persons: { add_persons: {
search_add_others_persons: "Rechercher et ajouter d'autres usagers",
title: "Ajouter des usagers", title: "Ajouter des usagers",
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats", suggested_counter: "Pas de résultats | 1 résultat | {count} résultats",
selected_counter: " 1 sélectionné | {count} sélectionnés", selected_counter: " 1 sélectionné | {count} sélectionnés",
@ -9,10 +8,16 @@ const personMessages = {
}, },
item: { item: {
type_person: "Usager", type_person: "Usager",
type_tms: "TMS", type_user: "TMS",
type_3rdparty: "Tiers", type_thirdparty: "Tiers",
type_menage: "Ménage" type_household: "Ménage"
} },
person: {
firstname: "Prénom",
lastname: "Nom",
born: "né le ",
},
error_only_one_person: "Une seule personne peut être sélectionnée !"
} }
}; };

View File

@ -12,18 +12,18 @@
</div> </div>
<div class="grid-4"> <div class="grid-4">
<ul class="record_actions">
<li>ponctuel <i class="fa fa-toggle-on fa-fw"></i> régulier</li>
<li>ouvert</li>
<li>en file active</li>
<li>urgent</li>
</ul>
</div> </div>
<div class="grid-3"> <div class="grid-3">
<p style="text-align: right;"> <p style="text-align: right;">
<i>ouvert le 11 avril 2019</i><br> {% if 'DRAFT' == accompanyingCourse.getStep() %}
par <b>Soline Maillet | SIPAS</b> Brouillon
{% else %}
<i>{{ 'Started on %date%'|trans({'%date%': accompanyingCourse.openingDate|format_date('short') } ) }}</i><br>
{% if accompanyingCourse.user is not null %}
par <b>{{ accompanyingCourse.user.username }}</b>
{% endif %}
{% endif %}
</p> </p>
</div> </div>

View File

@ -6,21 +6,23 @@
{% block content %} {% block content %}
<h1>{{ block('title') }}</h1> {% if 'DRAFT' == accompanyingCourse.step %}
<div class="grid-8 centered error flash_message">
<span>
{{ 'This accompanying course is still a draft'|trans }}
<a href="{{ path('chill_person_accompanying_course_show', { 'accompanying_period_id': accompanyingCourse.id } ) }}">
{{ 'Edit & activate accompanying course'|trans }}
</a>
</span>
</div>
{% endif %}
<pre> <h1>{{ 'Associated peoples'|trans }}</h1>
{{ accompanyingCourse.id }}
{{ accompanyingCourse.openingDate|format_date('short') }}
{{ accompanyingCourse.closingDate|format_date('short') }}
{{ accompanyingCourse.closingMotive|chill_entity_render_box }}
{{ accompanyingCourse.remark|raw }}
{{ accompanyingCourse.user }}
usagers:
{% for p in accompanyingCourse.participations %}
{{ p.person.id }} | <a href="{{ path('chill_person_accompanying_period_list', { person_id: p.person.id }) }}">{{ p.person.fullnamecanonical }}</a> | {{ p.startdate|format_date('short') }} | {{ p.enddate|format_date('short') }}
{% endfor %}
</pre>
{{ dump() }} <h1>{{ 'Resources'|trans }}</h1>
<h1>{{ 'Social actions'|trans }}</h1>
<h1>{{ 'Last events on accompanying course'|trans }}</h1>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,9 @@
{% extends 'ChillPersonBundle:AccompanyingCourse:layout.html.twig' %} {% extends 'ChillPersonBundle:AccompanyingCourse:layout.html.twig' %}
{% set title = 'DRAFT' == accompanyingCourse.step ? 'New accompanying course' : 'Edit accompanying course' %}
{% block title %} {% block title %}
{{ 'Edit Accompanying Course'|trans }} {{ title|trans }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -49,4 +49,22 @@
{{ form_end(form) }} {{ form_end(form) }}
NEW FORM
{% block content %}
<h1>{{ block('title') }}</h1>
<div id="address"></div>
{% endblock %}
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('address') }}
{% endblock %}
{% endblock personcontent %} {% endblock personcontent %}

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

@ -1,59 +0,0 @@
<?php
/*
*
* Copyright (C) 2014-2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class AccompanyingPeriodNormalizer implements NormalizerInterface, NormalizerAwareInterface {
protected ?NormalizerInterface $normalizer = null;
public function normalize($period, string $format = null, array $context = array())
{
/** @var AccompanyingPeriod $period */
return [
'id' => $period->getId(),
'openingDate' => $this->normalizer->normalize($period->getOpeningDate(), $format),
'closingDate' => $this->normalizer->normalize($period->getClosingDate(), $format),
'remark' => $period->getRemark(),
'participations' => $this->normalizer->normalize($period->getParticipations(), $format),
'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format),
'user' => $this->normalizer->normalize($period->getUser(), $format),
'step' => $period->getStep(),
'origin' => $this->normalizer->normalize($period->getOrigin(), $format),
'intensity' => $period->getIntensity(),
'emergency' => $period->isEmergency(),
'confidential' => $period->isConfidential()
];
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof AccompanyingPeriod;
}
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
}

View File

@ -41,6 +41,7 @@ class AccompanyingPeriodParticipationNormalizer implements NormalizerInterface,
public function supportsNormalization($data, string $format = null): bool public function supportsNormalization($data, string $format = null): bool
{ {
return false;
return $data instanceof AccompanyingPeriodParticipation; return $data instanceof AccompanyingPeriodParticipation;
} }

View File

@ -0,0 +1,85 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use Symfony\Component\Serializer\Exception;
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
private ResourceRepository $repository;
public function __construct(ResourceRepository $repository)
{
$this->repository = $repository;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$resource = $this->extractObjectToPopulate($type, $context);
if ('accompanying_period_resource' !== ($data['type'] ?? NULL)) {
throw new Exception\InvalidArgumentException("the key type must be present in data and set to 'accompanying_period_resource'");
}
if ($resource === NULL && \array_key_exists('id', $data)) {
$resource = $this->repository->find($data['id']);
if (NULL === $resource) {
throw new Exception\UnexpectedValueException(sprintf("the resource with".
"id %d is not found", $data['id']));
}
// if resource found, available only for read-only
if (count($data) > 2) {
unset($data['id']);
unset($data['type']);
throw new Exception\ExtraAttributesException($data);
}
}
if ($resource === NULL) {
$resource = new Resource();
}
if (\array_key_exists('resource', $data)) {
$res = $this->denormalizer->denormalize(
$data['resource'],
DiscriminatedObjectDenormalizer::TYPE,
$format,
\array_merge(
$context,
[
DiscriminatedObjectDenormalizer::ALLOWED_TYPES =>
[
Person::class, ThirdParty::class
]
]
)
);
$resource->setResource($res);
}
return $resource;
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return $type === Resource::class;
}
}

View File

@ -25,7 +25,8 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/** /**
* Serialize a Person entity * Serialize a Person entity
@ -33,60 +34,52 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException;
*/ */
class PersonNormalizer implements class PersonNormalizer implements
NormalizerInterface, NormalizerInterface,
NormalizerAwareInterface, NormalizerAwareInterface
DenormalizerInterface
{ {
protected NormalizerInterface $normalizer; protected NormalizerInterface $normalizer;
protected PersonRepository $repository; private ChillEntityRenderExtension $render;
public const GET_PERSON = 'get_person'; public function __construct(ChillEntityRenderExtension $render)
public function __construct(PersonRepository $repository)
{ {
$this->repository = $repository; $this->render = $render;
} }
public function normalize($person, string $format = null, array $context = array()) public function normalize($person, string $format = null, array $context = array())
{ {
/** @var Person $person */ /** @var Person $person */
return [ return [
'type' => 'person',
'id' => $person->getId(), 'id' => $person->getId(),
'text' => $this->render->renderString($person),
'firstName' => $person->getFirstName(), 'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(), 'lastName' => $person->getLastName(),
'birthdate' => $this->normalizer->normalize($person->getBirthdate()), 'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
'center' => $this->normalizer->normalize($person->getCenter()) 'center' => $this->normalizer->normalize($person->getCenter()),
'phonenumber' => $person->getPhonenumber(),
'mobilenumber' => $person->getMobilenumber(),
'altNames' => $this->normalizeAltNames($person->getAltNames())
]; ];
} }
public function denormalize($data, string $type, string $format = null, array $context = []): Person protected function normalizeAltNames($altNames): array
{ {
if ($context[self::GET_PERSON] ?? true) { $r = [];
$id = $data['id'] ?? null;
if (NULL === $id) {
throw new RuntimeException("missing id into person object");
}
}
/** var Person $person */
$person = $this->repository->findOneById($id);
if (NULL === $person) { foreach ($altNames as $n) {
return UnexpectedValueException("person id not found"); $r[] = [ 'key' => $n->getKey(), 'label' => $n->getLabel() ];
} }
return $person; return $r;
} }
public function supportsNormalization($data, string $format = null): bool public function supportsNormalization($data, string $format = null): bool
{ {
return $data instanceof Person; return $data instanceof Person;
} }
public function supportsDenormalization($data, string $type, ?string $format = NULL): bool
{
return Person::class === $type;
}
public function setNormalizer(NormalizerInterface $normalizer) public function setNormalizer(NormalizerInterface $normalizer)
{ {

View File

@ -0,0 +1,43 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
class SocialIssueNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
private SocialIssueRender $render;
use NormalizerAwareTrait;
/**
* @param SocialIssueRender $render
*/
public function __construct(SocialIssueRender $render)
{
$this->render = $render;
}
public function normalize($socialIssue, string $format = null, array $context = [])
{
/** @var SocialIssue $socialIssue */
return [
'type' => 'social_issue',
'id' => $socialIssue->getId(),
'parent_id' => $socialIssue->hasParent() ? $socialIssue->getParent()->getId() : null,
'children_ids' => $socialIssue->getChildren()->map(function (SocialIssue $si) { return $si->getId(); }),
'title' => $socialIssue->getTitle(),
'text' => $this->render->renderString($socialIssue, [])
];
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof SocialIssue;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Templating\TranslatableStringHelper;
class SocialIssueRender implements ChillEntityRenderInterface
{
private TranslatableStringHelper $translatableStringHelper;
public const SEPARATOR_KEY = 'default.separator';
public const DEFAULT_ARGS = [
self::SEPARATOR_KEY => ' > ',
];
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function supports($entity, array $options): bool
{
return $entity instanceof SocialIssueRender;
}
public function renderString($socialIssue, array $options): string
{
/** @var $socialIssue SocialIssue */
$options = \array_merge(self::DEFAULT_ARGS, $options);
$str = $this->translatableStringHelper->localize($socialIssue->getTitle());
while ($socialIssue->hasParent()) {
$socialIssue = $socialIssue->getParent();
$str .= $options[self::SEPARATOR_KEY].$this->translatableStringHelper->localize(
$socialIssue->getTitle()
);
}
return $str;
}
public function renderBox($entity, array $options): string
{
return "renderBox not implemented for social issue";
}
}

View File

@ -27,11 +27,14 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
/** /**
* Test api for AccompanyingCourseControllerTest * Test api for AccompanyingCourseControllerTest
@ -40,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)
*/ */
@ -85,6 +92,310 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)"); $this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)");
} }
/**
*
* @dataProvider dataGenerateRandomAccompanyingCourseWithSocialIssue
*/
public function testAccompanyingCourseAddRemoveSocialIssue(AccompanyingPeriod $period, SocialIssue $si)
{
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/socialissue.json', $period->getId()),
[],
[],
[],
\json_encode([ 'type' => 'social_issue', 'id' => $si->getId() ])
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$data = \json_decode($this->client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('type', $data);
$this->assertEquals('social_issue', $data['type']);
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/socialissue.json', $period->getId()),
[],
[],
[],
\json_encode([ 'type' => 'social_issue', 'id' => $si->getId() ])
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider dataGenerateRandomRequestorValidData
*/
public function testCommentWithValidData(AccompanyingPeriod $period, $personId, $thirdPartyId)
{
$em = self::$container->get(EntityManagerInterface::class);
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/comment.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'accompanying_period_comment', 'content' => "this is a text"])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('id', $data);
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/comment.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'accompanying_period_comment', 'id' => $data['id']])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @dataProvider dataGenerateRandomRequestorValidData
*/
public function testResourceWithValidData(AccompanyingPeriod $period, $personId, $thirdPartyId)
{
$em = self::$container->get(EntityManagerInterface::class);
// post a person
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'accompanying_period_resource', 'resource' => [ 'type' => 'person', 'id' => $personId ]])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('id', $data);
$this->assertEquals($personId, $data['resource']['id']);
// check into database
$resource = $em->getRepository(Resource::class)
->find($data['id']);
$this->assertInstanceOf(Resource::class, $resource);
$this->assertInstanceOf(Person::class, $resource->getResource());
$this->assertEquals($personId, $resource->getResource()->getId());
// remove the resource
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[],
[],
[], //server
\json_encode([ 'type' => 'accompanying_period_resource', 'id' => $resource->getId()])
);
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'accompanying_period_resource', 'resource' => [ 'type' => 'thirdparty', 'id' => $thirdPartyId ]])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('id', $data);
$this->assertEquals($thirdPartyId, $data['resource']['id']);
// check into database
$resource = $em->getRepository(Resource::class)
->find($data['id']);
$this->assertInstanceOf(Resource::class, $resource);
$this->assertInstanceOf(ThirdParty::class, $resource->getResource());
$this->assertEquals($thirdPartyId, $resource->getResource()->getId());
// remove the resource
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[],
[],
[], //server
\json_encode([ 'type' => 'accompanying_period_resource', 'id' => $resource->getId()])
);
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @dataProvider dataGenerateRandomRequestorValidData
*/
public function testRequestorWithValidData(AccompanyingPeriod $period, $personId, $thirdPartyId)
{
$em = self::$container->get(EntityManagerInterface::class);
// post a person
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'person', 'id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('id', $data);
$this->assertEquals($personId, $data['id']);
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$this->assertInstanceOf(Person::class, $period->getRequestor());
$this->assertEquals($personId, $period->getRequestor()->getId());
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'thirdparty', 'id' => $thirdPartyId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('id', $data);
$this->assertEquals($thirdPartyId, $data['id']);
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$this->assertInstanceOf(ThirdParty::class, $period->getRequestor());
$this->assertEquals($thirdPartyId, $period->getRequestor()->getId());
// remove the requestor
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId())
);
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$em->refresh($period);
$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
*/
public function testAccompanyingPeriodPatch(int $personId, AccompanyingPeriod $period)
{
$initialValueEmergency = $period->isEmergency();
$em = self::$container->get(EntityManagerInterface::class);
$this->client->request(
Request::METHOD_PATCH,
sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'type' => 'accompanying_period', 'emergency' => !$initialValueEmergency ])
);
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$em->refresh($period);
$this->assertEquals(!$initialValueEmergency, $period->isEmergency());
// restore the initial valud
$period->setEmergency($initialValueEmergency);
$em->flush();
}
public function dataGenerateRandomRequestorValidData(): \Iterator
{
$dataLength = 2;
$maxResults = 100;
static::bootKernel();
$em = static::$container->get(EntityManagerInterface::class);
$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($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
$thirdPartyIds = $em->createQuery("SELECT t.id FROM ".
ThirdParty::class." t ")
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($thirdPartyIds);
$i = 0;
while ($i <= $dataLength) {
$person = $em->getRepository(Person::class)
->find(\array_pop($personIds)['id']);
if (count($person->getAccompanyingPeriods()) === 0) {
continue;
}
$period = $person->getAccompanyingPeriods()[0];
yield [$period, \array_pop($personIds)['id'], \array_pop($thirdPartyIds)['id'] ];
$i++;
}
}
/** /**
* *
* @dataProvider dataGenerateRandomAccompanyingCourse * @dataProvider dataGenerateRandomAccompanyingCourse
@ -97,7 +408,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters [], // parameters
[], // files [], // files
[], // server parameters [], // server parameters
\json_encode([ 'id' => $personId ]) \json_encode([ 'type' => 'person', 'id' => $personId ])
); );
$response = $this->client->getResponse(); $response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true); $data = \json_decode($response->getContent(), true);
@ -128,7 +439,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters [], // parameters
[], // files [], // files
[], // server parameters [], // server parameters
\json_encode([ 'id' => $personId ]) \json_encode([ 'type' => 'person', 'id' => $personId ])
); );
$response = $this->client->getResponse(); $response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true); $data = \json_decode($response->getContent(), true);
@ -148,17 +459,10 @@ 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);
// remove participation if set
if ($this->personId && $this->period) {
$participation = $em $participation = $em
->getRepository(AccompanyingPeriodParticipation::class) ->getRepository(AccompanyingPeriodParticipation::class)
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period]) ->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
@ -168,6 +472,67 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$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;
}
}
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
{
// note about max result for person query, and maxGenerated:
//
// in the final loop, an id is popped out of the personIds array twice:
//
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
// Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :)
$maxGenerated = 3;
$maxResults = $maxGenerated * 8;
static::bootKernel();
$em = static::$container->get(EntityManagerInterface::class);
$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($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
$socialIssues = $em->createQuery("SELECT s FROM ".
SocialIssue::class." s ")
->setMaxResults(10)
->getResult();
$nbGenerated = 0;
while ($nbGenerated < $maxGenerated) {
$id = \array_pop($personIds)["id"];
$person = $em->getRepository(Person::class)
->find($id);
$periods = $person->getAccompanyingPeriods();
yield [$periods[\array_rand($periods)], $socialIssues[\array_rand($socialIssues)] ];
$nbGenerated++;
}
} }
public function dataGenerateRandomAccompanyingCourse() public function dataGenerateRandomAccompanyingCourse()
@ -211,4 +576,39 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$nbGenerated++; $nbGenerated++;
} }
} }
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

@ -11,7 +11,7 @@ class PersonDuplicateControllerViewTest extends WebTestCase
{ {
static::bootKernel(); static::bootKernel();
$this->em = static::$kernel->getContainer() $this->em = static::$container
->get('doctrine.orm.entity_manager'); ->get('doctrine.orm.entity_manager');
$center = $this->em->getRepository('ChillMainBundle:Center') $center = $this->em->getRepository('ChillMainBundle:Center')

View File

@ -0,0 +1,55 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Test\PrepareClientTrait;
class SocialIssueApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function setUp()
{
parent::setUp();
self::bootKernel();
}
public function testList(): array
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/person/social-work/social-issue.json');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThan(0, $data['count']);
$this->assertGreaterThan(0, count($data['results']));
return $data;
}
/**
* @depends testList
*/
public function testItem(array $data): void
{
$socialIssues = $data['results'];
shuffle($socialIssues);
$socialIssue = \array_pop($socialIssues);
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, sprintf('/api/1.0/person/social-work/social-issue/%d.json', $socialIssue['id']));
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $data);
$this->assertArrayHasKey('type', $data);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Chill\PersonBundle\Tests\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
class ResourceTest extends TestCase
{
public function testSetResource()
{
$person = new Person();
$thirdParty = new ThirdParty();
$resource = new Resource();
$resource->setResource($person);
$this->assertSame($person, $resource->getResource());
$this->assertNull($resource->getThirdParty());
$resource->setResource($thirdParty);
$this->assertSame($thirdParty, $resource->getResource());
$this->assertNull($resource->getPerson());
// we repeat adding a person, to ensure that third party is
// well reset
$resource->setResource($person);
$this->assertSame($person, $resource->getResource());
$this->assertNull($resource->getThirdParty());
$resource->setResource(null);
$this->assertNull($resource->getThirdParty());
$this->assertNull($resource->getPerson());
$this->assertNull($resource->getResource());
}
}

View File

@ -24,6 +24,8 @@ namespace Chill\PersonBundle\Tests\Entity;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
{ {
@ -104,4 +106,56 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$participation = $period->getOpenParticipationContainsPerson($person); $participation = $period->getOpenParticipationContainsPerson($person);
$this->assertNull($participation); $this->assertNull($participation);
} }
public function testRequestor()
{
$period = new AccompanyingPeriod(new \DateTime());
$person = new Person();
$thirdParty = new ThirdParty();
$this->assertNull($period->getRequestorThirdParty());
$this->assertNull($period->getRequestorPerson());
$this->assertNull($period->getRequestor());
$period->setRequestor($person);
$this->assertNull($period->getRequestorThirdParty());
$this->assertSame($person, $period->getRequestorPerson());
$this->assertSame($person, $period->getRequestor());
$period->setRequestor($thirdParty);
$this->assertNull($period->getRequestorPerson());
$this->assertSame($thirdParty, $period->getRequestorThirdParty());
$this->assertSame($thirdParty, $period->getRequestor());
$period->setRequestor(NULL);
$this->assertNull($period->getRequestorThirdParty());
$this->assertNull($period->getRequestorPerson());
$this->assertNull($period->getRequestor());
}
public function testInitialComment()
{
$period = new AccompanyingPeriod(new \DateTime());
$comment = new Comment();
$replacingComment = new Comment();
$period->setInitialComment(NULL);
$this->assertNull($period->getInitialComment());
$period->setInitialComment($comment);
$this->assertSame($period->getInitialComment(), $comment);
$this->assertSame($period, $comment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), "The initial comment should not appears in the list of comments");
$period->setInitialComment($replacingComment);
$this->assertSame($period->getInitialComment(), $replacingComment);
$this->assertSame($period, $replacingComment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), "The initial comment should not appears in the list of comments");
$this->assertNull($comment->getAccompanyingPeriod());
$period->setInitialComment(NULL);
$this->assertNull($period->getInitialComment());
$this->assertNull($replacingComment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), "The initial comment should not appears in the list of comments");
}
} }

Some files were not shown because too many files have changed in this diff Show More