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/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2"
"symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8"
},
"scripts": {
"auto-scripts": {

View File

@ -13,6 +13,9 @@ API
Chill provides a basic framework to build REST api.
Basic configuration
*******************
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>`_
Auto-loading the routes
***********************
=======================
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 on the usual way:
@ -87,7 +90,7 @@ Create your model on the usual way:
Configure api
*************
=============
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 are default actions:
@ -189,7 +192,7 @@ Entity:
Path: :code:`/api/1.0/person/accompanying-period/origin/{id}.{_format}`
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.
@ -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
Customize the controller
========================
************************
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
Create 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
============================
****************************
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
@ -400,10 +693,11 @@ This can be achieved quickly by assembling results into a :code:`Chill\MainBundl
}
}
.. _api_full_configuration:
Full configuration example
==========================
**************************
.. 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
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:

View File

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

View File

@ -5,7 +5,7 @@ namespace Chill\MainBundle\CRUD\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
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\PaginatorInterface;
@ -25,12 +25,19 @@ class AbstractCRUDController extends AbstractController
*
* @param string $id
* @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())
->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');
}
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 Chill\MainBundle\Serializer\Model\Collection;
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
{
@ -38,11 +42,6 @@ class ApiController extends AbstractCRUDController
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;
@ -81,13 +80,123 @@ class ApiController extends AbstractCRUDController
{
switch ($request->getMethod()) {
case Request::METHOD_GET:
case REQUEST::METHOD_HEAD:
case Request::METHOD_HEAD:
return $this->entityGet('_entity', $request, $id, $_format);
case Request::METHOD_PUT:
case Request::METHOD_PATCH:
return $this->entityPut('_entity', $request, $id, $_format);
default:
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
*/
@ -172,6 +281,110 @@ class ApiController extends AbstractCRUDController
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
@ -189,7 +402,26 @@ class ApiController extends AbstractCRUDController
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;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Search\UnknowSearchDomainException;
@ -34,6 +35,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi;
/**
* Class SearchController
@ -42,32 +44,24 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
*/
class SearchController extends AbstractController
{
/**
*
* @var SearchProvider
*/
protected $searchProvider;
protected SearchProvider $searchProvider;
/**
*
* @var TranslatorInterface
*/
protected $translator;
protected TranslatorInterface $translator;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
protected PaginatorFactory $paginatorFactory;
protected SearchApi $searchApi;
function __construct(
SearchProvider $searchProvider,
TranslatorInterface $translator,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
SearchApi $searchApi
) {
$this->searchProvider = $searchProvider;
$this->translator = $translator;
$this->paginatorFactory = $paginatorFactory;
$this->searchApi = $searchApi;
}
@ -152,6 +146,19 @@ class SearchController extends AbstractController
array('results' => $results, 'pattern' => $pattern)
);
}
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)
{

View File

@ -55,11 +55,11 @@ class LoadCenters extends AbstractFixture implements OrderedFixtureInterface
public function load(ObjectManager $manager)
{
foreach (static::$centers as $new) {
$centerA = new Center();
$centerA->setName($new['name']);
$center = new Center();
$center->setName($new['name']);
$manager->persist($centerA);
$this->addReference($new['ref'], $centerA);
$manager->persist($center);
$this->addReference($new['ref'], $center);
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\Reference;
use Chill\MainBundle\Doctrine\DQL\Replace;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ChillMainExtension
@ -133,7 +134,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/search.yaml');
$loader->load('services/serializer.yaml');
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
}
/**
@ -212,6 +213,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->prependExtensionConfig('monolog', array(
'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
}
/**
* @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_DELETE)->defaultFalse()->end()
->booleanNode(Request::METHOD_PUT)->defaultFalse()->end()
->booleanNode(Request::METHOD_PATCH)->defaultFalse()->end()
->end()
->end()
->arrayNode('roles')
@ -232,6 +233,7 @@ class Configuration implements ConfigurationInterface
->scalarNode(Request::METHOD_POST)->defaultNull()->end()
->scalarNode(Request::METHOD_DELETE)->defaultNull()->end()
->scalarNode(Request::METHOD_PUT)->defaultNull()->end()
->scalarNode(Request::METHOD_PATCH)->defaultNull()->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;
use Chill\MainBundle\Entity\AddressReferenceRepository;
use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Doctrine\Model\Point;
/**
* @ORM\Entity(repositoryClass=AddressReferenceRepository::class)
* @ORM\Entity()
* @ORM\Table(name="chill_main_address_reference")
* @ORM\HasLifecycleCallbacks()
*/

View File

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

View File

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

View File

@ -39,8 +39,17 @@ div.subheader {
height: 130px;
}
//// VUEJS ////
//// SCRATCH BUTTONS
.sc-button {
&.disabled {
cursor: default;
&.bt-remove {
background-color: #d9d9d9;
}
}
}
//// VUEJS ////
div.vue-component {
padding: 1.5em;
margin: 2em 0;
@ -95,33 +104,47 @@ div.vue-component {
}
//// AddPersons modal
div.modal-body.up {
margin: auto 4em;
div.search {
position: relative;
input {
padding: 1.2em 1.5em 1.2em 2.5em;
margin: 1em 0;
div.body-head {
overflow-y: unset;
div.modal-body:first-child {
margin: auto 4em;
div.search {
position: relative;
input {
padding: 1.2em 1.5em 1.2em 2.5em;
margin: 1em 0;
}
i {
position: absolute;
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;
}
}
i {
position: absolute;
top: 50%;
left: 0.5em;
padding: 0.65em 0;
opacity: 0.5;
}
}
div.modal-body:last-child {
padding-bottom: 0;
}
}
div.results {
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
a {
cursor: pointer;
}
}
div.results {
div.list-item {
line-height: 26pt;
padding: 0.3em 0.8em;
padding: 0.4em 0.8em;
display: flex;
flex-direction: row;
&.checked {
@ -132,11 +155,20 @@ div.results {
& > input {
margin-right: 0.8em;
}
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
div.right_actions {
margin: 0 0 0 auto;
display: flex;
align-items: flex-end;
& > * {
margin-left: 0.5em;
align-self: baseline;
}
a.sc-button {
border: 1px solid lightgrey;
@ -146,8 +178,19 @@ div.results {
}
}
}
.discret {
color: grey;
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')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="modal-body up" style="overflow-y: unset;">
<slot name="body-fixed"></slot>
<div class="body-head">
<slot name="body-head"></slot>
</div>
<div class="modal-body">
<slot name="body"></slot>

View File

@ -7,11 +7,15 @@ const datetimeFormats = {
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "short",
month: "numeric",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
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;
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())
{
/** @var \DateTimeInterface $date */
return [
'datetime' => $date->format(\DateTimeInterface::ISO8601),
'u' => $date->getTimestamp()
'datetime' => $date->format(\DateTimeInterface::ISO8601)
];
}
@ -36,4 +37,24 @@ class DateNormalizer implements NormalizerInterface
{
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
{
@ -32,8 +32,10 @@ class UserNormalizer implements NormalizerInterface
{
/** @var User $user */
return [
'type' => 'user',
'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)
{
var_dump($data);
$query = $this->getExport()->initiateQuery($modifiers, $acl, $data);
$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);
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:
_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:
path: /{_locale}/search/advanced/{name}
controller: Chill\MainBundle\Controller\SearchController::advancedSearchAction

View File

@ -3,6 +3,18 @@ parameters:
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:
class: Chill\MainBundle\Templating\TranslatableStringHelper
arguments:

View File

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

View File

@ -1,3 +1,10 @@
services:
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:
Chill\MainBundle\Serializer\Normalizer\CenterNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }
# note: the autowiring for serializers and normalizers is declared
# into ../services.yaml
Chill\MainBundle\Serializer\Normalizer\DateNormalizer:
Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer:
autowire: true
tags:
- { name: 'serializer.normalizer', priority: 64 }
- { name: 'serializer.normalizer', priority: 8 }
Chill\MainBundle\Serializer\Normalizer\UserNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }
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 Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
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
{
@ -19,10 +25,37 @@ class AccompanyingCourseApiController extends ApiController
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->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)
@ -46,7 +79,6 @@ class AccompanyingCourseApiController extends ApiController
break;
case Request::METHOD_DELETE:
$participation = $accompanyingPeriod->removePerson($person);
$participation->setEndDate(new \DateTimeImmutable('now'));
break;
default:
throw new BadRequestException("This method is not supported");
@ -56,12 +88,76 @@ class AccompanyingCourseApiController extends ApiController
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors);
return $this->json($errors, 422);
}
$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

View File

@ -6,6 +6,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -42,6 +43,41 @@ class AccompanyingCourseController extends Controller
$this->dispatcher = $dispatcher;
$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
*
@ -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
/*
* 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->prependDoctrineDQL($container);
$this->prependCruds($container);
$this->prependWorkflows($container);
//add person_fields parameter as global
$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
*
@ -321,8 +354,15 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
'roles' => [
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' => [
@ -336,8 +376,79 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_POST => \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;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\User;
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
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
{
/**
* Mark an accompanying period as "occasional"
@ -80,6 +89,7 @@ class AccompanyingPeriod
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @Groups({"read"})
*/
private $id;
@ -87,6 +97,7 @@ class AccompanyingPeriod
* @var \DateTime
*
* @ORM\Column(type="date")
* @Groups({"read", "write"})
*/
private $openingDate;
@ -94,6 +105,7 @@ class AccompanyingPeriod
* @var \DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Groups({"read", "write"})
*/
private $closingDate = null;
@ -101,6 +113,7 @@ class AccompanyingPeriod
* @var string
*
* @ORM\Column(type="text")
* @Groups({"read", "write"})
*/
private $remark = '';
@ -108,17 +121,28 @@ class AccompanyingPeriod
* @var Collection
*
* @ORM\OneToMany(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Comment",
* mappedBy="accompanyingPeriod"
* mappedBy="accompanyingPeriod",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
*/
private $comments;
/**
* @ORM\ManyToOne(
* targetEntity=Comment::class
* )
* @Groups({"read"})
*/
private ?Comment $initialComment = null;
/**
* @var Collection
*
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
* mappedBy="accompanyingPeriod",
* cascade={"persist", "refresh", "remove", "merge", "detach"})
* @Groups({"read"})
*/
private $participations;
@ -128,36 +152,42 @@ class AccompanyingPeriod
* @ORM\ManyToOne(
* targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/
private $closingMotive = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/
private $user;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
*/
private $createdBy;
/**
* @var string
* @ORM\Column(type="string", length=32, nullable=true)
* @Groups({"read"})
*/
private $step = self::STEP_DRAFT;
/**
* @ORM\ManyToOne(targetEntity=Origin::class)
* @ORM\JoinColumn(nullable=true)
* @Groups({"read", "write"})
*/
private $origin;
/**
* @var string
* @ORM\Column(type="string", nullable=true)
* @Groups({"read", "write"})
*/
private $intensity;
@ -172,6 +202,7 @@ class AccompanyingPeriod
* joinColumns={@ORM\JoinColumn(name="accompanying_period_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="scope_id", referencedColumnName="id")}
* )
* @Groups({"read"})
*/
private $scopes;
@ -189,19 +220,22 @@ class AccompanyingPeriod
/**
* @var bool
* @ORM\Column(type="boolean")
* @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/
private $requestorAnonymous = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/
private $emergency = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @ORM\Column(type="boolean", options={"default": false} )
* @Groups({"read", "write"})
*/
private $confidential = false;
@ -210,21 +244,54 @@ class AccompanyingPeriod
*
* @ORM\OneToMany(
* targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Resource",
* mappedBy="accompanyingPeriod"
* mappedBy="accompanyingPeriod",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
* @Groups({"read"})
*/
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.
*
* @param \DateTime $dateOpening
* @uses AccompanyingPeriod::setClosingDate()
*/
public function __construct(\DateTime $dateOpening) {
$this->setOpeningDate($dateOpening);
public function __construct(\DateTime $dateOpening = null) {
$this->setOpeningDate($dateOpening ?? new \DateTime('now'));
$this->participations = new ArrayCollection();
$this->scopes = new ArrayCollection();
$this->socialIssues = new ArrayCollection();
$this->comments = new ArrayCollection();
}
/**
@ -318,23 +385,55 @@ class AccompanyingPeriod
return $this->remark;
}
/**
* @Groups({"read"})
*/
public function getComments(): Collection
{
return $this->comments;
return $this->comments->filter(function (Comment $c) {
return $c !== $this->initialComment;
});
}
public function addComment(Comment $comment): self
{
$this->comments[] = $comment;
$comment->setAccompanyingPeriod($this);
return $this;
}
public function removeComment(Comment $comment): void
{
$comment->setAccompanyingPeriod(null);
$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
*/
@ -515,9 +614,9 @@ class AccompanyingPeriod
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;
}
@ -527,21 +626,53 @@ class AccompanyingPeriod
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 Person|ThirdParty
* @Groups({"read"})
*/
public function getRequestor()
{
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
{
return $this->requestorAnonymous;
@ -638,6 +769,7 @@ class AccompanyingPeriod
public function addResource(Resource $resource): self
{
$resource->setAccompanyingPeriod($this);
$this->resources[] = $resource;
return $this;
@ -645,9 +777,27 @@ class AccompanyingPeriod
public function removeResource(Resource $resource): void
{
$resource->setAccompanyingPeriod(null);
$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
*/
@ -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\PersonBundle\Entity\AccompanyingPeriod;
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\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\GeneratedValue
* @ORM\Column(type="integer")
* @Groups({"read"})
*/
private $id;
@ -50,27 +58,32 @@ class Comment
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/
private $creator;
/**
* @ORM\Column(type="datetime")
* @Groups({"read"})
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Groups({"read"})
*/
private $updatedAt;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/
private $updatedBy;
/**
* @ORM\Column(type="text")
* @Groups({"read", "write"})
*/
private $content;
@ -103,6 +116,11 @@ class Comment
return $this;
}
public function setCreatedBy(User $user): self
{
return $this->setCreator($user);
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
@ -132,7 +150,7 @@ class Comment
return $this->updatedBy;
}
public function setUpdatedBy(?User $updatedBy): self
public function setUpdatedBy(User $updatedBy): self
{
$this->updatedBy = $updatedBy;

View File

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

View File

@ -25,12 +25,18 @@ namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
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")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period_resource"=Resource::class
* })
*/
class Resource
{
@ -38,6 +44,7 @@ class Resource
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Groups({"read"})
*/
private $id;
@ -90,7 +97,7 @@ class Resource
return $this->thirdParty;
}
public function setThirdParty(?ThirdParty $thirdParty): self
private function setThirdParty(?ThirdParty $thirdParty): self
{
$this->thirdParty = $thirdParty;
@ -102,7 +109,7 @@ class Resource
return $this->person;
}
public function setPerson(?Person $person): self
private function setPerson(?Person $person): self
{
$this->person = $person;
@ -120,9 +127,35 @@ class Resource
return $this;
}
/**
*
* @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 Person|ThirdParty
* @return ThirdParty|Person
* @Groups({"read", "write"})
*/
public function getResource()
{

View File

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

View File

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

View File

@ -4,10 +4,15 @@ namespace Chill\PersonBundle\Entity\SocialWork;
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;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_social_issue")
* @DiscriminatorMap(typeProperty="type", mapping={
* "social_issue"=SocialIssue::class
* })
*/
class SocialIssue
{
@ -35,6 +40,7 @@ class SocialIssue
/**
* @ORM\Column(type="json")
* @Groups({"read"})
*/
private $title = [];
@ -59,6 +65,11 @@ class SocialIssue
return $this->parent;
}
public function hasParent(): bool
{
return $this->parent !== null;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;

View File

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

View File

@ -71,6 +71,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'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;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Resource|null find($id, $lockMode = null, $lockVersion = null)
@ -32,12 +33,12 @@ use Doctrine\ORM\EntityRepository;
* @method Resource[] findAll()
* @method Resource[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
final class ResourceRepository
final class ResourceRepository extends ServiceEntityRepository
{
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>
<persons-associated></persons-associated>
<requestor></requestor>
<social-issue></social-issue>
<referrer></referrer>
<resources></resources>
<comment></comment>
<confirm></confirm>
<!--test></test-->
</template>
<script>
@ -10,13 +16,25 @@ import { mapState } from 'vuex'
import AccompanyingCourse from './components/AccompanyingCourse.vue';
import PersonsAssociated from './components/PersonsAssociated.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 {
name: 'App',
components: {
AccompanyingCourse,
PersonsAssociated,
Requestor
Requestor,
SocialIssue,
Referrer,
Resources,
Comment,
Confirm,
//Test
},
computed: mapState([
'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
* method GET, get AccompanyingCourse Object
* Endpoint v.2 chill_api_single_accompanying_course__entity
* method GET/HEAD, get AccompanyingCourse Instance
*
* @accompanying_period_id___ integer
* @TODO var is not used but necessary in method signature
* @id integer - id of accompanyingCourse
*/
let getAccompanyingCourse = (accompanying_period_id___) => { //tmp
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/show.${format}`;
const getAccompanyingCourse = (id) => {
const url = `/api/1.0/person/accompanying-course/${id}.json`;
return fetch(url)
.then(response => {
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
*
* @accompanying_period_id integer - id of accompanyingCourse
* @person_id integer - id of person
* @method string - POST or DELETE
* @id integer - id of accompanyingCourse
* @payload integer - id of person
* @method string - POST or DELETE
*/
let postParticipation = (accompanying_period_id, person_id, method) => {
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/participation.${format}`
const postParticipation = (id, payload, method) => {
const body = { type: payload.type, id: payload.id };
const url = `/api/1.0/person/accompanying-course/${id}/participation.json`;
return fetch(url, {
method: method,
headers: {
'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 => {
if (response.ok) { return response.json(); }
@ -44,6 +120,9 @@ let postParticipation = (accompanying_period_id, person_id, method) => {
};
export {
getAccompanyingCourse,
postParticipation
getAccompanyingCourse,
patchAccompanyingCourse,
postParticipation,
postRequestor,
postResource,
};

View File

@ -1,24 +1,37 @@
<template>
<div class="vue-component">
<div class="vue-component" style="display: none;">
<h3>{{ $t('course.title') }}</h3>
<dl>
<dt>{{ $t('course.id') }}</dt>
<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>
<dt>{{ $t('course.id') }}</dt>
<dd>{{ accompanyingCourse.id }}</dd>
</dl>
</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>
<script>
import ToggleFlags from './ToggleFlags';
export default {
name: 'AccompanyingCourse',
components: {
ToggleFlags
},
computed: {
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>
</li-->
<li>
<button class="sc-button bt-remove"
<button v-if="!participation.endDate"
class="sc-button bt-remove"
:title="$t('action.remove')"
@click.prevent="$emit('close', participation)">
</button>
<button v-else class="sc-button bt-remove disabled"></button>
</li>
</ul>
</td>

View File

@ -2,7 +2,7 @@
<div class="vue-component">
<h3>{{ $t('persons_associated.title')}}</h3>
<label>{{ $tc('persons_associated.counter', counter) }}</label>
<table class="rounded">
<table class="rounded">
<thead>
<tr>
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th>
@ -12,24 +12,26 @@
<th class="chill-orange">{{ $t('action.actions') }}</th>
</tr>
</thead>
<tbody>
<person-item
v-for="participation in participations"
v-bind:participation="participation"
v-bind:key="participation.id"
<tbody>
<person-item
v-for="participation in participations"
v-bind:participation="participation"
v-bind:key="participation.id"
@remove="removeParticipation"
@close="closeParticipation">
</person-item>
</tbody>
</table>
<add-persons></add-persons>
<ul class="record_actions">
<!--li>
<button class="sc-button orange" @click="savePersons">
{{ $t('action.save') }}
</button>
</li-->
</ul>
<add-persons
buttonTitle="persons_associated.add_persons"
modalTitle="add_persons.title"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
@ -40,27 +42,44 @@ import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
export default {
name: 'PersonsAssociated',
components: {
components: {
PersonItem,
AddPersons
},
data() {
return {
addPersons: {
key: 'persons_associated',
options: {
type: ['person'],
priority: null,
uniq: false,
}
}
}
},
computed: mapState({
participations: state => state.accompanyingCourse.participations,
counter: state => state.accompanyingCourse.participations.length
}),
methods: {
removeParticipation(item) {
this.$store.dispatch('removeParticipation', item)
console.log('@@ CLICK remove participation: item', item);
this.$store.dispatch('removeParticipation', item);
},
closeParticipation(item) {
console.log('@@ CLICK close participation: item', item);
this.$store.dispatch('closeParticipation', item)
this.$store.dispatch('closeParticipation', item);
},
/*
savePersons() {
console.log('[wip] saving persons');
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addParticipation', 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('referrer.title') }}</h3>
</div>
</template>
<script>
export default {
name: "Referrer",
}
</script>

View File

@ -1,85 +1,112 @@
<template>
<div class="vue-component">
<h3>{{ $t('requestor.title') }}</h3>
{{ accompanyingCourse.id }}
{{ accompanyingCourse.remark }}<br><br>
<!-- TESTS AREA -->
<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>
<div v-if="accompanyingCourse.requestor">
<label>
<input type="checkbox" v-model="isAnonymous" :value="value" />
{{ $t('requestor.is_anonymous') }}
</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>
<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 TESTS -->
<button v-if="accompanyingCourse.requestor !== null"
class="sc-button bt-remove"
:title="$t('action.remove')"
@click="removeRequestor">
</button>
<add-persons v-if="accompanyingCourse.requestor === null"
buttonTitle="requestor.add_requestor"
modalTitle="requestor.add_requestor"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
export default {
name: 'Requestor',
components: {
Modal,
AddPersons,
},
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
addPersons: {
key: 'requestor',
options: {
type: ['person', 'thirdparty'],
priority: null,
uniq: true,
}
}
}
},
computed: {
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,19 +9,70 @@ const appMessages = {
closing_date: "Date de clôture",
remark: "Commentaire",
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: {
title: "Usagers concernés",
counter: "Pas d'usager | 1 usager | {count} usagers",
firstname: "Prénom",
lastname: "Nom",
startdate: "Date d'entrée",
firstname: "Prénom",
lastname: "Nom",
startdate: "Date d'entrée",
enddate: "Date de sortie",
addPerson: "Ajouter un usager",
add_persons: "Ajouter des usagers",
},
requestor: {
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 { createStore } from 'vuex';
import addPersons from './modules/addPersons'
import { getAccompanyingCourse, postParticipation } from '../api';
import { getAccompanyingCourse,
patchAccompanyingCourse,
postParticipation,
postRequestor,
postResource } from '../api';
const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId; //tmp
const id = window.accompanyingCourseId;
let initPromise = getAccompanyingCourse(id)
.then(accompanying_course => new Promise((resolve, reject) => {
@ -13,7 +15,6 @@ let initPromise = getAccompanyingCourse(id)
const store = createStore({
strict: debug,
modules: {
addPersons
},
state: {
accompanyingCourse: accompanying_course,
@ -22,54 +23,154 @@ let initPromise = getAccompanyingCourse(id)
getters: {
},
mutations: {
removeParticipation(state, item) {
//console.log('mutation: remove item', item.id);
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== item);
catchError(state, error) {
state.errorMsg.push(error);
},
removeParticipation(state, participation) {
//console.log('### mutation: remove participation', participation.id);
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(element => element !== participation);
},
closeParticipation(state, { participation, payload }) {
console.log('### mutation: close item', { participation, payload });
// trouve dans le state le payload et le supprime du state
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== payload);
// pousse la participation
state.accompanyingCourse.participations.push(participation);
//console.log('### mutation: close item', { participation, payload });
// find row position and replace by closed participation
state.accompanyingCourse.participations.splice(
state.accompanyingCourse.participations.findIndex(element => element === payload), 1, participation
);
},
addParticipation(state, participation) {
//console.log('### mutation: add participation', 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: {
removeParticipation({ commit }, payload) {
commit('removeParticipation', payload);
// fetch DELETE request...
},
closeParticipation({ commit }, payload) {
//console.log('## action: fetch delete participation: payload', payload.person.id);
postParticipation(id, payload.person.id, 'DELETE')
.then(participation => new Promise((resolve, reject) => {
//console.log('payload', payload);
commit('closeParticipation', { participation, payload });
resolve();
}))
.catch((error) => {
state.errorMsg.push(error.message);
});
//console.log('## action: fetch delete participation: payload', payload);
postParticipation(id, payload.person, 'DELETE')
.then(participation => new Promise((resolve, reject) => {
commit('closeParticipation', { participation, payload });
resolve();
})).catch((error) => { commit('catchError', error) });
},
addParticipation(addPersons, payload) {
//console.log('## action: fetch post participation: payload', payload.id);
postParticipation(id, payload.id, 'POST')
.then(participation => new Promise((resolve, reject) => {
//console.log(participation, payload);
addPersons.commit('addParticipation', participation);
addPersons.commit('resetState', payload);
resolve();
}))
.catch((error) => {
state.errorMsg.push(error.message);
});
addParticipation({ commit }, payload) {
//console.log('## action: fetch post participation (select item): payload', payload);
postParticipation(id, payload.result, 'POST')
.then(participation => new Promise((resolve, reject) => {
commit('addParticipation', participation);
resolve();
})).catch((error) => { commit('catchError', error) });
},
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);
}));

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',
format = 'json'
;
/*
* Build query string with query and options
*/
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
*/
let searchPersons = (query) => {
let url = `/${locale}/search.${format}?name=person_regular&q=${query}`;
const searchPersons = ({ query, options }) => {
let queryStr = parametersToString({ query, options });
let url = `/fr/search.json?name=person_regular&${queryStr}`;
return fetch(url)
.then(response => {
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,73 +1,87 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_persons.search_add_others_persons') }}
{{ $t(buttonTitle) }}
</button>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('add_persons.title') }}</h3>
<h3 class="modal-title">{{ $t(modalTitle) }}</h3>
</template>
<template v-slot:body-fixed>
<div class="search">
<label style="float: right;">
{{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label>
<input id="search-persons"
name="query"
v-model="query"
:placeholder="$t('add_persons.search_some_persons')"
ref="search" />
<i class="fa fa-search fa-lg"></i>
<template v-slot:body-head>
<div class="modal-body">
<div class="search">
<label style="float: right;">
{{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label>
<input id="search-persons"
name="query"
v-model="query"
:placeholder="$t('add_persons.search_some_persons')"
ref="search" />
<i class="fa fa-search fa-lg"></i>
<i class="fa fa-times" v-if="queryLength >= 3" @click="resetSuggestion"></i>
</div>
</div>
</template>
<template v-slot:body>
<!--span class="discret">Selection: {{ selected }}</span-->
<div class="results">
<div class="modal-body" v-if="checkUniq === 'checkbox'">
<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>
<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>
<person-suggestion
</div>
</template>
<template v-slot:body>
<!--span class="discret">Selection: {{ selected }}</span-->
<div class="results">
<person-suggestion
v-for="item in this.selectedAndSuggested.slice().reverse()"
v-bind:item="item"
v-bind:key="item.id">
v-bind:key="itemKey(item)"
v-bind:item="item"
v-bind:search="search"
v-bind:type="checkUniq"
@updateSelected="updateSelected">
</person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
{{ $t('action.create') }} "{{ query }}"
</button>
</div>
</template>
<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')}}
</button>
</template>
</modal>
</teleport>
</template>
<script>
import { mapState } from 'vuex';
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 {
name: 'AddPersons',
@ -75,40 +89,69 @@ export default {
Modal,
PersonSuggestion,
},
props: [
'buttonTitle',
'modalTitle',
'options'
],
emits: ['addNewPersons'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
search: {
query: "",
suggested: [],
selected: []
}
}
},
computed: {
...mapState({
addPersons: state => state.addPersons
}),
query: {
set(query) {
this.$store.dispatch('setQuery', { query });
return this.setQuery(query);
},
get() {
return this.addPersons.query;
return this.search.query;
}
},
queryLength() {
return this.search.query.length;
},
suggested() {
return this.addPersons.suggested;
return this.search.suggested;
},
suggestedCounter() {
return this.addPersons.suggested.length;
},
return this.search.suggested.length;
},
selected() {
return this.addPersons.selected;
},
return this.search.selected;
},
selectedCounter() {
return this.addPersons.selected.length;
return this.search.selected.length;
},
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: {
@ -118,15 +161,49 @@ export default {
this.$refs.search.focus();
})
},
addNewPersons() {
console.log('@@@ CLICK button addPersons')
this.selected.forEach(function(item) {
//console.log('# dispatch action for each item', item);
this.$store.dispatch('addParticipation', item);
}, this
);
this.modal.showModal = false;
setQuery(query) {
this.search.query = query;
if (query.length >= 3) {
searchPersons_2({ query, options: this.options })
.then(suggested => new Promise((resolve, reject) => {
console.log('suggested', suggested);
this.loadSuggestions(suggested.results);
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>

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 = {
fr: {
add_persons: {
search_add_others_persons: "Rechercher et ajouter d'autres usagers",
title: "Ajouter des usagers",
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats",
selected_counter: " 1 sélectionné | {count} sélectionnés",
@ -9,10 +8,16 @@ const personMessages = {
},
item: {
type_person: "Usager",
type_tms: "TMS",
type_3rdparty: "Tiers",
type_menage: "Ménage"
}
type_user: "TMS",
type_thirdparty: "Tiers",
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 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 class="grid-3">
<p style="text-align: right;">
<i>ouvert le 11 avril 2019</i><br>
par <b>Soline Maillet | SIPAS</b>
{% if 'DRAFT' == accompanyingCourse.getStep() %}
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>
</div>

View File

@ -6,21 +6,23 @@
{% 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>
{{ 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>
<h1>{{ 'Associated peoples'|trans }}</h1>
{{ dump() }}
<h1>{{ 'Resources'|trans }}</h1>
<h1>{{ 'Social actions'|trans }}</h1>
<h1>{{ 'Last events on accompanying course'|trans }}</h1>
{% endblock %}

View File

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

View File

@ -49,4 +49,22 @@
{{ 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 %}

View File

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

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
{
return false;
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 Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* Serialize a Person entity
@ -33,60 +34,52 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException;
*/
class PersonNormalizer implements
NormalizerInterface,
NormalizerAwareInterface,
DenormalizerInterface
NormalizerAwareInterface
{
protected NormalizerInterface $normalizer;
protected PersonRepository $repository;
private ChillEntityRenderExtension $render;
public const GET_PERSON = 'get_person';
public function __construct(PersonRepository $repository)
public function __construct(ChillEntityRenderExtension $render)
{
$this->repository = $repository;
$this->render = $render;
}
public function normalize($person, string $format = null, array $context = array())
{
/** @var Person $person */
return [
'type' => 'person',
'id' => $person->getId(),
'text' => $this->render->renderString($person),
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'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) {
$id = $data['id'] ?? null;
if (NULL === $id) {
throw new RuntimeException("missing id into person object");
}
}
/** var Person $person */
$person = $this->repository->findOneById($id);
$r = [];
if (NULL === $person) {
return UnexpectedValueException("person id not found");
foreach ($altNames as $n) {
$r[] = [ 'key' => $n->getKey(), 'label' => $n->getLabel() ];
}
return $person;
return $r;
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof Person;
}
public function supportsDenormalization($data, string $type, ?string $format = NULL): bool
{
return Person::class === $type;
}
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\AccompanyingPeriodParticipation;
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\Center;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
/**
* Test api for AccompanyingCourseControllerTest
@ -40,6 +43,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
{
protected static EntityManagerInterface $em;
protected ?int $personId = NULL;
protected ?AccompanyingPeriod $period = NULL;
/**
* 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)");
}
/**
*
* @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
@ -97,7 +408,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
\json_encode([ 'type' => 'person', 'id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
@ -128,7 +439,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
\json_encode([ 'type' => 'person', 'id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
@ -148,25 +459,79 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
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);
$participation = $em
->getRepository(AccompanyingPeriodParticipation::class)
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
;
// remove participation if set
if ($this->personId && $this->period) {
$participation = $em
->getRepository(AccompanyingPeriodParticipation::class)
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
;
if (NULL !== $participation) {
$em->remove($participation);
$em->flush();
if (NULL !== $participation) {
$em->remove($participation);
$em->flush();
}
$this->personId = NULL;
$this->period = NULL;
} elseif ($this->period) {
$period = $em
->getRepository(AccompanyingPeriod::class)
->find($this->period->getId()) ;
if ($period !== NULL) {
$em->remove($period);
$em->flush();
}
$this->period = null;
}
}
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
{
// 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++;
}
}
@ -211,4 +576,39 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$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();
$this->em = static::$kernel->getContainer()
$this->em = static::$container
->get('doctrine.orm.entity_manager');
$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\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
{
@ -104,4 +106,56 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$participation = $period->getOpenParticipationContainsPerson($person);
$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