diff --git a/composer.json b/composer.json index 4181af68a..95074c18f 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/docs/source/development/api.rst b/docs/source/development/api.rst index 86eb6ff65..a5fef2372 100644 --- a/docs/source/development/api.rst +++ b/docs/source/development/api.rst @@ -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 `_ 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[^/]++)/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 `). +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 `). .. 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 diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 6aee5c526..1312480e3 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -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: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9d74dd61a..ab9e69052 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,11 +18,10 @@ src/Bundle/ChillMainBundle/Tests/ - diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index 5cc055f26..df7d065cb 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -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'); + } } diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index 7643db0e9..14b0473da 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -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' ]]; } /** diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php index a44af8e7b..45390ceca 100644 --- a/src/Bundle/ChillMainBundle/Controller/SearchController.php +++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php @@ -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) { diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadCenters.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadCenters.php index 1da1af2f3..a3c01fdf6 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadCenters.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadCenters.php @@ -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']; } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index a40221263..277e916a8 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -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 + ] + ], + ] + ] + ] + ]); + } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index c7e4c00ef..4c90aaabb 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -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() diff --git a/src/Bundle/ChillMainBundle/Doctrine/Event/TrackCreateUpdateSubscriber.php b/src/Bundle/ChillMainBundle/Doctrine/Event/TrackCreateUpdateSubscriber.php new file mode 100644 index 000000000..3926e2062 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/Event/TrackCreateUpdateSubscriber.php @@ -0,0 +1,65 @@ +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')); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationInterface.php b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationInterface.php new file mode 100644 index 000000000..192d4e7a9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationInterface.php @@ -0,0 +1,12 @@ + + * @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 = []; diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 1a46b30a5..11d168f84 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -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 { diff --git a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss index a27dcdc42..6cf4d23f2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss @@ -39,114 +39,17 @@ div.subheader { height: 130px; } -//// VUEJS //// - -div.vue-component { - padding: 1.5em; - margin: 2em 0; - border: 2px dashed grey; - position: relative; - &:before { - content: "vuejs component"; - position: absolute; - left: 1.5em; - top: -0.9em; - background-color: white; - color: grey; - padding: 0 0.3em; - } - dd { margin-left: 1em; } -} - -//// MODAL //// -.modal-mask { - position: fixed; - z-index: 9998; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.75); - display: table; - transition: opacity 0.3s ease; -} -.modal-header .close { // bootstrap classes, override sc-button 0 radius - border-top-right-radius: 0.3rem; -} - -/* -* The following styles are auto-applied to elements with -* transition="modal" when their visibility is toggled -* by Vue.js. -* -* You can easily play with the modal transition by editing -* these styles. -*/ -.modal-enter { - opacity: 0; -} -.modal-leave-active { - opacity: 0; -} -.modal-enter .modal-container, -.modal-leave-active .modal-container { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -//// 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; - } - i { - position: absolute; - top: 50%; - left: 0.5em; - padding: 0.65em 0; - opacity: 0.5; - } - - } -} -div.results { - div.count { - margin: -0.5em 0 0.7em; - display: flex; - justify-content: space-between; - } - div.list-item { - line-height: 26pt; - padding: 0.3em 0.8em; - display: flex; - flex-direction: row; - &.checked { - background-color: #ececec; - border-bottom: 1px dotted #8b8b8b; - } - div.container { - & > input { - margin-right: 0.8em; - } - } - div.right_actions { - margin: 0 0 0 auto; - & > * { - margin-left: 0.5em; - } - a.sc-button { - border: 1px solid lightgrey; - font-size: 70%; - padding: 4px; - } +//// SCRATCH BUTTONS +.sc-button { + &.disabled { + cursor: default; + &.bt-remove { + background-color: #d9d9d9; } } } +//// à ranger .discret { color: grey; margin-right: 1em; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue new file mode 100644 index 000000000..461b0038e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/App.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js new file mode 100644 index 000000000..27676369e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js @@ -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: ``, +}) +.use(store) +.use(i18n) +.component('app', App) +.mount('#address'); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/js/i18n.js new file mode 100644 index 000000000..87ee9a5c0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/js/i18n.js @@ -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 +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/store/index.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/store/index.js new file mode 100644 index 000000000..b9466b50c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/store/index.js @@ -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 }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_api/AddAddress.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_api/AddAddress.js new file mode 100644 index 000000000..4de0fcc19 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_api/AddAddress.js @@ -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 +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress.vue new file mode 100644 index 000000000..55cf2f098 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress.vue @@ -0,0 +1,219 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMap.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMap.vue new file mode 100644 index 000000000..5b616819d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMap.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMore.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMore.vue new file mode 100644 index 000000000..b216d5ed0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressMore.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressSelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressSelection.vue new file mode 100644 index 000000000..71738079c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/AddressSelection.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CitySelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CitySelection.vue new file mode 100644 index 000000000..99e46db08 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CitySelection.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CountrySelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CountrySelection.vue new file mode 100644 index 000000000..4eb591135 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddAddress/CountrySelection.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue index cbd2d0738..7498d66f3 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue @@ -1,41 +1,40 @@ + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js index 4f5c64e30..dee070804 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js @@ -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 diff --git a/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig index 132e81a3e..0d23d499d 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig @@ -1,4 +1,4 @@

{{ 'This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License'|trans|raw }} -
{{ 'User manual'|trans }}

-
\ No newline at end of file +
{{ 'User manual'|trans }}

+ diff --git a/src/Bundle/ChillMainBundle/Resources/views/Layout/_header-logo.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Layout/_header-logo.html.twig index be59b454f..f619dc151 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Layout/_header-logo.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Layout/_header-logo.html.twig @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/Bundle/ChillMainBundle/Search/Model/Result.php b/src/Bundle/ChillMainBundle/Search/Model/Result.php new file mode 100644 index 000000000..a88c2f55f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Search/Model/Result.php @@ -0,0 +1,36 @@ +relevance = $relevance; + $this->result = $result; + } + + public function getRelevance(): float + { + return $this->relevance; + } + + public function getResult() + { + return $this->result; + } + + + +} diff --git a/src/Bundle/ChillMainBundle/Search/SearchApi.php b/src/Bundle/ChillMainBundle/Search/SearchApi.php new file mode 100644 index 000000000..518edd31b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Search/SearchApi.php @@ -0,0 +1,89 @@ +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) + ; + } + +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php new file mode 100644 index 000000000..29d760b71 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php @@ -0,0 +1,29 @@ +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; + } + + +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php index fd902b184..8e333a2a4 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php @@ -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)); + } } diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php new file mode 100644 index 000000000..25b2c5017 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php @@ -0,0 +1,71 @@ +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; + } + +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DoctrineExistingEntityNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DoctrineExistingEntityNormalizer.php new file mode 100644 index 000000000..cb2635802 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DoctrineExistingEntityNormalizer.php @@ -0,0 +1,71 @@ +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; + } +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php index 2a71de52b..4c1dc1523 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php @@ -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() ]; } diff --git a/src/Bundle/ChillMainBundle/Test/Export/AbstractExportTest.php b/src/Bundle/ChillMainBundle/Test/Export/AbstractExportTest.php index d258c0d30..e480ab145 100644 --- a/src/Bundle/ChillMainBundle/Test/Export/AbstractExportTest.php +++ b/src/Bundle/ChillMainBundle/Test/Export/AbstractExportTest.php @@ -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, diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DoctrineExistingEntityNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DoctrineExistingEntityNormalizerTest.php new file mode 100644 index 000000000..c4f4bc13d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DoctrineExistingEntityNormalizerTest.php @@ -0,0 +1,51 @@ +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'] ]; + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml new file mode 100644 index 000000000..ada65b08a --- /dev/null +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -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" + diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 6187b31ea..78accf004 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -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'); }; diff --git a/src/Bundle/ChillMainBundle/config/routes.yaml b/src/Bundle/ChillMainBundle/config/routes.yaml index 3fd7eafab..bc3187add 100644 --- a/src/Bundle/ChillMainBundle/config/routes.yaml +++ b/src/Bundle/ChillMainBundle/config/routes.yaml @@ -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 diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index 93a4ee4e4..a6afa4336 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -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: diff --git a/src/Bundle/ChillMainBundle/config/services/controller.yaml b/src/Bundle/ChillMainBundle/config/services/controller.yaml index 5fb542786..6021e3d72 100644 --- a/src/Bundle/ChillMainBundle/config/services/controller.yaml +++ b/src/Bundle/ChillMainBundle/config/services/controller.yaml @@ -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: diff --git a/src/Bundle/ChillMainBundle/config/services/search.yaml b/src/Bundle/ChillMainBundle/config/services/search.yaml index e8a457415..b7a1656b3 100644 --- a/src/Bundle/ChillMainBundle/config/services/search.yaml +++ b/src/Bundle/ChillMainBundle/config/services/search.yaml @@ -1,3 +1,10 @@ services: chill_main.search_provider: - class: Chill\MainBundle\Search\SearchProvider \ No newline at end of file + 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' diff --git a/src/Bundle/ChillMainBundle/config/services/serializer.yaml b/src/Bundle/ChillMainBundle/config/services/serializer.yaml index fb5f57b7e..c7cc6ca63 100644 --- a/src/Bundle/ChillMainBundle/config/services/serializer.yaml +++ b/src/Bundle/ChillMainBundle/config/services/serializer.yaml @@ -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 } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index bbf2f399a..0d8189b6a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -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 diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php index 05a1934e6..6786cb05f 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php @@ -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); - } } diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 98e2d30e9..0d6346c01 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -1,5 +1,4 @@ * @@ -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 + ] + ], + ] + ], ] ]); } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 8ec017c07..d5b43446c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -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; + } } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php index 60b3466cd..0d64da0fc 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php @@ -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; diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php index a852e166e..3175e3156 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php @@ -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; diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php index 0cfefba87..cf796722a 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php @@ -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() { diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php index d57d00386..70c5a0505 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php @@ -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; diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index e88998205..1afa6278c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -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 { diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php index 2b6df9be2..6b2152cdf 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php @@ -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; diff --git a/src/Bundle/ChillPersonBundle/Menu/AccompanyingCourseMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/AccompanyingCourseMenuBuilder.php index 74052e87c..e9ce20b53 100644 --- a/src/Bundle/ChillPersonBundle/Menu/AccompanyingCourseMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/AccompanyingCourseMenuBuilder.php @@ -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]); } diff --git a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php index ea6a1d060..0b307204d 100644 --- a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php @@ -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' ] + ]); } /** diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/ResourceRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/ResourceRepository.php index 4f625b59c..c32f74762 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/ResourceRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/ResourceRepository.php @@ -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); } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue index 480b882dc..7f9b66326 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue @@ -1,25 +1,94 @@ + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js index 0f71f7170..8b7a4ad6b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js @@ -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,131 @@ 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 to change 'DRAFT' step to 'CONFIRMED' +*/ +const confirmAccompanyingCourse = (id) => { + const url = `/api/1.0/person/accompanying-course/${id}/confirm.json` + return fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'} + }) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + +/* +* Endpoint +*/ +const getSocialIssues = () => { + const url = `/api/1.0/person/social-work/social-issue.json`; + return fetch(url) + .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 +147,11 @@ let postParticipation = (accompanying_period_id, person_id, method) => { }; export { - getAccompanyingCourse, - postParticipation + getAccompanyingCourse, + patchAccompanyingCourse, + confirmAccompanyingCourse, + getSocialIssues, + postParticipation, + postRequestor, + postResource, }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/AccompanyingCourse.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/AccompanyingCourse.vue deleted file mode 100644 index be3014ddb..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/AccompanyingCourse.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue new file mode 100644 index 000000000..1a85e4bb7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue new file mode 100644 index 000000000..c3e83a548 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue new file mode 100644 index 000000000..c2143b72a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonItem.vue index 0d81d61c4..4fa2caf12 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonItem.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonItem.vue @@ -29,10 +29,12 @@
  • - +
  • diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue index 631534fcb..9c31c7ef5 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue @@ -1,8 +1,12 @@ @@ -40,27 +48,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; } - */ } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue new file mode 100644 index 000000000..39952e687 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue index 6086ab515..6a2c6d5cc 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue @@ -1,85 +1,118 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue new file mode 100644 index 000000000..d69c3d0ea --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue new file mode 100644 index 000000000..a756727d8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue @@ -0,0 +1,60 @@ + + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Test.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Test.vue new file mode 100644 index 000000000..a72902b25 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Test.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ToggleFlags.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ToggleFlags.vue new file mode 100644 index 000000000..67ef8c193 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ToggleFlags.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/js/i18n.js index 045476686..93de282db 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/js/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/js/i18n.js @@ -4,24 +4,82 @@ const appMessages = { fr: { course: { id: "id", - title: "Parcours", + title: { + draft: "Création d'un nouveau parcours", + active: "Modification du parcours" + }, opening_date: "Date d'ouverture", closing_date: "Date de clôture", remark: "Commentaire", closing_motive: "Motif de clôture", + user: "TMS", + flags: "Indicateurs", + status: "État", + step: { + draft: "Brouillon", + 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", + counter: "Il n'y a pas encore d'usager | 1 usager | {count} usagers", + 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", + label: "Choisir les problématiques sociales", + }, + referrer: { + title: "Référent", + }, + resources: { + title: "Interlocuteurs privilégiés", + counter: "Il n'y a pas encore d'interlocuteur | 1 interlocuteur | {count} interlocuteurs", + text: "Dénomination", + description: "Description", + add_resources: "Ajouter des interlocuteurs", + }, + comment: { + title: "Observations", + label: "Ajout d'une note", + content: "Rédigez une première note..." + }, + confirm: { + title: "Confirmation", + text_draft: "Le parcours est actuellement à l'état de ", + text_active: "En validant cette étape, vous lui donnez le statut ", + sure: "Êtes-vous sûr ?", + sure_description: "Une fois le changement confirmé, il n'est plus possible de le remettre à l'état de brouillon !", + ok: "Confirmer le parcours" + }, + } }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js index 04785eca5..0f7a4f188 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js @@ -1,11 +1,14 @@ import 'es6-promise/auto'; import { createStore } from 'vuex'; -import addPersons from './modules/addPersons' -import { getAccompanyingCourse, postParticipation } from '../api'; +import { getAccompanyingCourse, + patchAccompanyingCourse, + confirmAccompanyingCourse, + 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 +16,6 @@ let initPromise = getAccompanyingCourse(id) const store = createStore({ strict: debug, modules: { - addPersons }, state: { accompanyingCourse: accompanying_course, @@ -22,54 +24,168 @@ 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; + }, + confirmAccompanyingCourse(state, response) { + //console.log('### mutation: confirmAccompanyingCourse: response', response); + state.accompanyingCourse.step = response.step; + } }, 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) }); + }, + + confirmAccompanyingCourse({ commit }) { + console.log('## action: confirmAccompanyingCourse'); + confirmAccompanyingCourse(id) + .then(response => new Promise((resolve, reject) => { + commit('confirmAccompanyingCourse', response); + console.log('fetch resolve'); // redirection with #top anchor + resolve(); + })).catch((error) => { commit('catchError', error) }); + } } }); - //console.log('store object', store.state.accompanyingCourse.id); resolve(store); })); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/modules/addPersons.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/modules/addPersons.js deleted file mode 100644 index e65f94fa9..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/modules/addPersons.js +++ /dev/null @@ -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 -} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/AddPersons.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/AddPersons.js index d5ac91ac5..cf2404288 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/AddPersons.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/AddPersons.js @@ -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 }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue index 53600da1c..a37f93e7e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue @@ -1,73 +1,91 @@