mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-26 16:45:01 +00:00
Compare commits
101 Commits
refactorin
...
fix-timeli
Author | SHA1 | Date | |
---|---|---|---|
|
8364518282 | ||
|
4e50d0ace9 | ||
|
0b54adeb59 | ||
|
5d23de4862 | ||
a63c38b6aa | |||
8915c42008 | |||
503293a359 | |||
7bdf71315a | |||
6f5d457122 | |||
b4274264a7 | |||
8002725c87 | |||
2c9edf3741 | |||
3f562449e1 | |||
1cce39bcb5 | |||
ad690db886 | |||
|
1b8462b40d | ||
|
8c98f2cf6e | ||
f3260f57a4 | |||
b4ad0128e4 | |||
33457d3cbc | |||
e919b4322e | |||
b6131379f9 | |||
e1fcc41ace | |||
f2a04cebe6 | |||
c693002ddb | |||
ee77c8540a | |||
a536d2780e | |||
f880598052 | |||
e7985ea52f | |||
f56dc65021 | |||
|
4770758aee | ||
90fe484d81 | |||
2b8bbe019d | |||
9e1c151402 | |||
088e522292 | |||
b8e6e98a74 | |||
07e0692783 | |||
e78d53064b | |||
e6fcb5ff22 | |||
f02e33fda7 | |||
1bbec9efae | |||
3b53c4451c | |||
30f490959b | |||
|
0a894b0db1 | ||
c20e9507d5 | |||
f89c690f1b | |||
|
f61af9d02a | ||
120e7cade5 | |||
ac550e05e8 | |||
|
86c177bbbb | ||
cc8de353d4 | |||
4495a5d33b | |||
9dffe30aad | |||
d393e74896 | |||
daf083dc88 | |||
|
ce859697b5 | ||
|
1587c762f8 | ||
|
17c01d9b46 | ||
a1bb9ea352 | |||
|
a1895ec65f | ||
|
c5faa0b99d | ||
|
e9d142f3e8 | ||
|
91860afd80 | ||
|
bec0700d39 | ||
|
05d3d7f5c7 | ||
|
ebff36d257 | ||
|
a709b3afb6 | ||
|
69a3c6a9b2 | ||
|
8c33d876e8 | ||
|
1ec2fbcc16 | ||
|
bc4e29141b | ||
|
e90ea31683 | ||
|
7c99f0b3e0 | ||
|
c089960707 | ||
|
0b2f29f1e8 | ||
|
9a4f50472a | ||
|
7d1a1c4004 | ||
|
ebd58d4229 | ||
|
dd48795f64 | ||
3d45b6687f | |||
35e6d36ce0 | |||
4900c81c11 | |||
ec68e6f761 | |||
25bd170f6b | |||
6939b4a313 | |||
d18922ed98 | |||
e8bf242c42 | |||
2050460bb3 | |||
|
4add30e895 | ||
e1dbfdafd7 | |||
75becb2294 | |||
94e494361f | |||
|
d27bdf68fb | ||
|
82b82920e1 | ||
4bb3eadf91 | |||
3a0c25c871 | |||
083338da8d | |||
37e5c9ceaa | |||
4cf77a9b0e | |||
|
c2a4a95eba | ||
01f9d03b14 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
.composer/*
|
||||
composer.phar
|
||||
composer.lock
|
||||
|
||||
docs/build/
|
||||
.php_cs.cache
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
@@ -19,3 +19,4 @@ docs/build/
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
|
453
docs/source/development/api.rst
Normal file
453
docs/source/development/api.rst
Normal file
@@ -0,0 +1,453 @@
|
||||
.. Copyright (C) 2014 Champs Libres Cooperative SCRLFS
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
|
||||
.. _api:
|
||||
|
||||
API
|
||||
###
|
||||
|
||||
Chill provides a basic framework to build REST api.
|
||||
|
||||
Configure a route
|
||||
=================
|
||||
|
||||
Follow those steps to build a REST api:
|
||||
|
||||
1. Create your model;
|
||||
2. Configure the API;
|
||||
|
||||
You can also:
|
||||
|
||||
* hook into the controller to customize some steps;
|
||||
* add more route and steps
|
||||
|
||||
.. note::
|
||||
|
||||
Useful links:
|
||||
|
||||
* `How to use annotation to configure serialization <https://symfony.com/doc/current/serializer.html>`_
|
||||
* `How to create your custom normalizer <https://symfony.com/doc/current/serializer/custom_normalizer.html>`_
|
||||
|
||||
Auto-loading the routes
|
||||
***********************
|
||||
|
||||
Ensure that those lines are present in your file `app/config/routing.yml`:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
chill_cruds:
|
||||
resource: 'chill_main_crud_route_loader:load'
|
||||
type: service
|
||||
|
||||
|
||||
Create your model
|
||||
*****************
|
||||
|
||||
Create your model on the usual way:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=OriginRepository::class)
|
||||
* @ORM\Table(name="chill_person_accompanying_period_origin")
|
||||
*/
|
||||
class Origin
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private $label;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date_immutable", nullable=true)
|
||||
*/
|
||||
private $noActiveAfter;
|
||||
|
||||
// .. getters and setters
|
||||
|
||||
}
|
||||
|
||||
|
||||
Configure api
|
||||
*************
|
||||
|
||||
Configure the api using Yaml (see the full configuration: :ref:`api_full_configuration`):
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# config/packages/chill_main.yaml
|
||||
chill_main:
|
||||
apis:
|
||||
accompanying_period_origin:
|
||||
base_path: '/api/1.0/person/accompanying-period/origin'
|
||||
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin'
|
||||
name: accompanying_period_origin
|
||||
base_role: 'ROLE_USER'
|
||||
actions:
|
||||
_index:
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
_entity:
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
|
||||
.. note::
|
||||
|
||||
If you are working on a shared bundle (aka "The chill bundles"), you should define your configuration inside the class :code:`ChillXXXXBundleExtension`, using the "prependConfig" feature:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Chill\PersonBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class ChillPersonExtension
|
||||
* Loads and manages your bundle configuration
|
||||
*
|
||||
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
|
||||
* @package Chill\PersonBundle\DependencyInjection
|
||||
*/
|
||||
class ChillPersonExtension extends Extension implements PrependExtensionInterface
|
||||
{
|
||||
public function prepend(ContainerBuilder $container)
|
||||
{
|
||||
$this->prependCruds($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
protected function prependCruds(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
'apis' => [
|
||||
[
|
||||
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class,
|
||||
'name' => 'accompanying_period_origin',
|
||||
'base_path' => '/api/1.0/person/accompanying-period/origin',
|
||||
'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
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
The :code:`_index` and :code:`_entity` action
|
||||
=============================================
|
||||
|
||||
The :code:`_index` and :code:`_entity` action are default actions:
|
||||
|
||||
* they will call a specific method in the default controller;
|
||||
* they will generate defined routes:
|
||||
|
||||
Index:
|
||||
Name: :code:`chill_api_single_accompanying_period_origin__index`
|
||||
|
||||
Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}`
|
||||
|
||||
Entity:
|
||||
Name: :code:`chill_api_single_accompanying_period_origin__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.
|
||||
|
||||
For index action, the role will be called with :code:`NULL` as :code:`$subject`. The retrieved entity will be the subject for single queries.
|
||||
|
||||
You can also define a role for each method. In this case, this role is used for the given method, and, if any, the base role is taken into account.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# config/packages/chill_main.yaml
|
||||
chill_main:
|
||||
apis:
|
||||
accompanying_period_origin:
|
||||
base_path: '/api/1.0/person/bla/bla'
|
||||
class: 'Chill\PersonBundle\Entity\Blah'
|
||||
name: bla
|
||||
actions:
|
||||
_entity:
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
roles:
|
||||
GET: MY_ROLE_SEE
|
||||
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`.
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class OpeningApiController extends ApiController
|
||||
{
|
||||
protected function customizeQuery(string $action, Request $request, $qb): void
|
||||
{
|
||||
$qb->where($qb->expr()->gt('e.noActiveAfter', ':now'))
|
||||
->orWhere($qb->expr()->isNull('e.noActiveAfter'));
|
||||
$qb->setParameter('now', new \DateTime('now'));
|
||||
}
|
||||
}
|
||||
|
||||
And set your controller in configuration:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
chill_main:
|
||||
apis:
|
||||
accompanying_period_origin:
|
||||
base_path: '/api/1.0/person/accompanying-period/origin'
|
||||
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin'
|
||||
name: accompanying_period_origin
|
||||
# add a controller
|
||||
controller: 'Chill\PersonBundle\Controller\OpeningApiController'
|
||||
base_role: 'ROLE_USER'
|
||||
actions:
|
||||
_index:
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
_entity:
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
|
||||
Create your own actions
|
||||
=======================
|
||||
|
||||
You can add your own actions:
|
||||
|
||||
.. 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:
|
||||
# add a custom participation:
|
||||
participation:
|
||||
methods:
|
||||
POST: true
|
||||
DELETE: true
|
||||
GET: false
|
||||
HEAD: false
|
||||
PUT: false
|
||||
roles:
|
||||
POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
|
||||
DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
|
||||
GET: null
|
||||
HEAD: null
|
||||
PUT: null
|
||||
single-collection: single
|
||||
|
||||
The key :code:`single-collection` with value :code:`single` will add a :code:`/{id}/ + "action name"` (in this example, :code:`/{id}/participation`) into the path, after the base path. If the value is :code:`collection`, no id will be set, but the action name will be append to the path.
|
||||
|
||||
Then, create the corresponding action into your controller:
|
||||
|
||||
.. 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\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;
|
||||
|
||||
class AccompanyingCourseApiController extends ApiController
|
||||
{
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected ValidatorInterface $validator;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, $validator)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function participationApi($id, Request $request, $_format)
|
||||
{
|
||||
/** @var AccompanyingPeriod $accompanyingPeriod */
|
||||
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
|
||||
$person = $this->getSerializer()
|
||||
->deserialize($request->getContent(), Person::class, $_format, []);
|
||||
|
||||
if (NULL === $person) {
|
||||
throw new BadRequestException('person id not found');
|
||||
}
|
||||
|
||||
$this->onPostCheckACL('participation', $request, $accompanyingPeriod, $_format);
|
||||
|
||||
switch ($request->getMethod()) {
|
||||
case Request::METHOD_POST:
|
||||
$participation = $accompanyingPeriod->addPerson($person);
|
||||
break;
|
||||
case Request::METHOD_DELETE:
|
||||
$participation = $accompanyingPeriod->removePerson($person);
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException("This method is not supported");
|
||||
}
|
||||
|
||||
$errors = $this->validator->validate($accompanyingPeriod);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
// only format accepted
|
||||
return $this->json($errors);
|
||||
}
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return $this->json($participation);
|
||||
}
|
||||
}
|
||||
|
||||
Serialization for collection
|
||||
============================
|
||||
|
||||
A specific model has been defined for returning collection:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"count": 49,
|
||||
"results": [
|
||||
],
|
||||
"pagination": {
|
||||
"more": true,
|
||||
"next": "/api/1.0/search.json&q=xxxx......&page=2",
|
||||
"previous": null,
|
||||
"first": 0,
|
||||
"items_per_page": 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
This can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination <pagination-ref>`).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
|
||||
class MyController extends AbstractController
|
||||
{
|
||||
|
||||
protected function serializeCollection(PaginatorInterface $paginator, $entities): Response
|
||||
{
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
return $this->json($model, Response::HTTP_OK, [], $context);
|
||||
}
|
||||
}
|
||||
|
||||
.. _api_full_configuration:
|
||||
|
||||
Full configuration example
|
||||
==========================
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
apis:
|
||||
-
|
||||
class: Chill\PersonBundle\Entity\AccompanyingPeriod
|
||||
name: accompanying_course
|
||||
base_path: /api/1.0/person/accompanying-course
|
||||
controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
|
||||
actions:
|
||||
_entity:
|
||||
roles:
|
||||
GET: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
|
||||
HEAD: null
|
||||
POST: null
|
||||
DELETE: null
|
||||
PUT: null
|
||||
controller_action: null
|
||||
path: null
|
||||
single-collection: single
|
||||
methods:
|
||||
GET: true
|
||||
HEAD: true
|
||||
POST: false
|
||||
DELETE: false
|
||||
PUT: false
|
||||
participation:
|
||||
methods:
|
||||
POST: true
|
||||
DELETE: true
|
||||
GET: false
|
||||
HEAD: false
|
||||
PUT: false
|
||||
roles:
|
||||
POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
|
||||
DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
|
||||
GET: null
|
||||
HEAD: null
|
||||
PUT: null
|
||||
controller_action: null
|
||||
# the requirements for the route. Will be set to `[ 'id' => '\d+' ]` if left empty.
|
||||
requirements: []
|
||||
path: null
|
||||
single-collection: single
|
||||
base_role: null
|
||||
|
||||
|
@@ -16,6 +16,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
|
||||
|
||||
Instructions to create a new bundle <create-a-new-bundle.rst>
|
||||
CRUD (Create - Update - Delete) for one entity <crud.rst>
|
||||
Helpers for building a REST API <api.rst>
|
||||
Routing <routing.rst>
|
||||
Menus <menus.rst>
|
||||
Forms <forms.rst>
|
||||
|
@@ -7,6 +7,8 @@
|
||||
Free Documentation License".
|
||||
|
||||
|
||||
.. _pagination-ref:
|
||||
|
||||
Pagination
|
||||
##########
|
||||
|
||||
@@ -15,7 +17,7 @@ The Bundle :code:`Chill\MainBundle` provides a **Pagination** api which allow yo
|
||||
A simple example
|
||||
****************
|
||||
|
||||
In the controller, get the :class:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance.
|
||||
In the controller, get the :code:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance.
|
||||
|
||||
|
||||
.. literalinclude:: pagination/example.php
|
||||
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\CRUD\CompilerPass;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Chill\MainBundle\Routing\MenuComposer;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class CRUDControllerCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$crudConfig = $container->getParameter('chill_main_crud_route_loader_config');
|
||||
$apiConfig = $container->getParameter('chill_main_api_route_loader_config');
|
||||
|
||||
foreach ($crudConfig as $crudEntry) {
|
||||
$this->configureCrudController($container, $crudEntry, 'crud');
|
||||
}
|
||||
|
||||
foreach ($apiConfig as $crudEntry) {
|
||||
$this->configureCrudController($container, $crudEntry, 'api');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a controller for each definition, and add a methodCall to inject crud configuration to controller
|
||||
*/
|
||||
private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void
|
||||
{
|
||||
$controllerClass = $crudEntry['controller'];
|
||||
|
||||
$controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller';
|
||||
|
||||
if ($container->hasDefinition($controllerClass)) {
|
||||
$controller = $container->getDefinition($controllerClass);
|
||||
$container->removeDefinition($controllerClass);
|
||||
$alreadyDefined = true;
|
||||
} else {
|
||||
$controller = new Definition($controllerClass);
|
||||
$alreadyDefined = false;
|
||||
}
|
||||
|
||||
$controller->addTag('controller.service_arguments');
|
||||
if (FALSE === $alreadyDefined) {
|
||||
$controller->setAutoconfigured(true);
|
||||
$controller->setPublic(true);
|
||||
}
|
||||
|
||||
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
|
||||
$container->setParameter($param, $crudEntry);
|
||||
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
|
||||
|
||||
$container->setDefinition($controllerServiceName, $controller);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
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 Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
|
||||
class AbstractCRUDController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* The crud configuration
|
||||
*
|
||||
* This configuration si defined by `chill_main['crud']` or `chill_main['apis']`
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $crudConfig = [];
|
||||
|
||||
/**
|
||||
* get the instance of the entity with the given id
|
||||
*
|
||||
* @param string $id
|
||||
* @return object
|
||||
*/
|
||||
protected function getEntity($action, $id, Request $request): ?object
|
||||
{
|
||||
return $this->getDoctrine()
|
||||
->getRepository($this->getEntityClass())
|
||||
->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of entities
|
||||
*
|
||||
* By default, count all entities. You can customize the query by
|
||||
* using the method `customizeQuery`.
|
||||
*
|
||||
* @param string $action
|
||||
* @param Request $request
|
||||
* @return int
|
||||
*/
|
||||
protected function countEntities(string $action, Request $request, $_format): int
|
||||
{
|
||||
return $this->buildQueryEntities($action, $request)
|
||||
->select('COUNT(e)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the entity.
|
||||
*
|
||||
* By default, get all entities. You can customize the query by using the
|
||||
* method `customizeQuery`.
|
||||
*
|
||||
* The method `orderEntity` is called internally to order entities.
|
||||
*
|
||||
* It returns, by default, a query builder.
|
||||
*
|
||||
*/
|
||||
protected function queryEntities(string $action, Request $request, string $_format, PaginatorInterface $paginator)
|
||||
{
|
||||
$query = $this->buildQueryEntities($action, $request)
|
||||
->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber())
|
||||
->setMaxResults($paginator->getItemsPerPage());
|
||||
|
||||
// allow to order queries and return the new query
|
||||
return $this->orderQuery($action, $query, $request, $paginator, $_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ordering fields in the query build by self::queryEntities
|
||||
*
|
||||
*/
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the base query for listing all entities.
|
||||
*
|
||||
* This method is used internally by `countEntities` `queryEntities`
|
||||
*
|
||||
* This base query does not contains any `WHERE` or `SELECT` clauses. You
|
||||
* can add some by using the method `customizeQuery`.
|
||||
*
|
||||
* The alias for the entity is "e".
|
||||
*
|
||||
* @param string $action
|
||||
* @param Request $request
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function buildQueryEntities(string $action, Request $request)
|
||||
{
|
||||
$qb = $this->getDoctrine()->getManager()
|
||||
->createQueryBuilder()
|
||||
->select('e')
|
||||
->from($this->getEntityClass(), 'e')
|
||||
;
|
||||
|
||||
$this->customizeQuery($action, $request, $qb);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
protected function customizeQuery(string $action, Request $request, $query): void {}
|
||||
|
||||
/**
|
||||
* Get the result of the query
|
||||
*/
|
||||
protected function getQueryResult(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query)
|
||||
{
|
||||
return $query->getQuery()->getResult();
|
||||
}
|
||||
|
||||
protected function onPreIndex(string $action, Request $request, string $_format): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* method used by indexAction
|
||||
*/
|
||||
protected function onPreIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* method used by indexAction
|
||||
*/
|
||||
protected function onPostIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* method used by indexAction
|
||||
*/
|
||||
protected function onPostIndexFetchQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $entities): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the complete FQDN of the class
|
||||
*
|
||||
* @return string the complete fqdn of the class
|
||||
*/
|
||||
protected function getEntityClass(): string
|
||||
{
|
||||
return $this->crudConfig['class'];
|
||||
}
|
||||
|
||||
/**
|
||||
* called on post fetch entity
|
||||
*/
|
||||
protected function onPostFetchEntity(string $action, Request $request, $entity, $_format): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on post check ACL
|
||||
*/
|
||||
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* check the acl. Called by every action.
|
||||
*
|
||||
* By default, check the role given by `getRoleFor` for the value given in
|
||||
* $entity.
|
||||
*
|
||||
* Throw an \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
|
||||
* if not accessible.
|
||||
*
|
||||
* @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
|
||||
*/
|
||||
protected function checkACL(string $action, Request $request, string $_format, $entity = null)
|
||||
{
|
||||
$this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string the crud name
|
||||
*/
|
||||
protected function getCrudName(): string
|
||||
{
|
||||
return $this->crudConfig['name'];
|
||||
}
|
||||
|
||||
protected function getActionConfig(string $action)
|
||||
{
|
||||
return $this->crudConfig['actions'][$action];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the crud configuration
|
||||
*
|
||||
* Used by the container to inject configuration for this crud.
|
||||
*/
|
||||
public function setCrudConfig(array $config): void
|
||||
{
|
||||
$this->crudConfig = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PaginatorFactory
|
||||
*/
|
||||
protected function getPaginatorFactory(): PaginatorFactory
|
||||
{
|
||||
return $this->container->get('chill_main.paginator_factory');
|
||||
}
|
||||
}
|
220
src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
Normal file
220
src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\CRUD\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
|
||||
class ApiController extends AbstractCRUDController
|
||||
{
|
||||
/**
|
||||
* The view action.
|
||||
*
|
||||
* Some steps may be overriden during this process of rendering:
|
||||
*
|
||||
* This method:
|
||||
*
|
||||
* 1. fetch the entity, using `getEntity`
|
||||
* 2. launch `onPostFetchEntity`. If postfetch is an instance of Response,
|
||||
* this response is returned.
|
||||
* 2. throw an HttpNotFoundException if entity is null
|
||||
* 3. check ACL using `checkACL` ;
|
||||
* 4. launch `onPostCheckACL`. If the result is an instance of Response,
|
||||
* this response is returned ;
|
||||
* 5. Serialize the entity and return the result. The serialization context is given by `getSerializationContext`
|
||||
*
|
||||
*/
|
||||
protected function entityGet(string $action, Request $request, $id, $_format = 'html'): 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;
|
||||
}
|
||||
|
||||
if ($_format === 'json') {
|
||||
$context = $this->getContextForSerialization($action, $request, $_format, $entity);
|
||||
|
||||
return $this->json($entity, Response::HTTP_OK, [], $context);
|
||||
} else {
|
||||
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public function onBeforeSerialize(string $action, Request $request, $_format, $entity): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base method for handling api action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function entityApi(Request $request, $id, $_format): Response
|
||||
{
|
||||
switch ($request->getMethod()) {
|
||||
case Request::METHOD_GET:
|
||||
case REQUEST::METHOD_HEAD:
|
||||
return $this->entityGet('_entity', $request, $id, $_format);
|
||||
default:
|
||||
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base action for indexing entities
|
||||
*/
|
||||
public function indexApi(Request $request, string $_format)
|
||||
{
|
||||
switch ($request->getMethod()) {
|
||||
case Request::METHOD_GET:
|
||||
case REQUEST::METHOD_HEAD:
|
||||
return $this->indexApiAction('_index', $request, $_format);
|
||||
default:
|
||||
throw $this->createNotFoundException("This method is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an index page.
|
||||
*
|
||||
* Some steps may be overriden during this process of rendering.
|
||||
*
|
||||
* This method:
|
||||
*
|
||||
* 1. Launch `onPreIndex`
|
||||
* x. check acl. If it does return a response instance, return it
|
||||
* x. launch `onPostCheckACL`. If it does return a response instance, return it
|
||||
* 1. count the items, using `countEntities`
|
||||
* 2. build a paginator element from the the number of entities ;
|
||||
* 3. Launch `onPreIndexQuery`. If it does return a response instance, return it
|
||||
* 3. build a query, using `queryEntities`
|
||||
* x. fetch the results, using `getQueryResult`
|
||||
* x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
|
||||
* 4. Serialize the entities in a Collection, using `SerializeCollection`
|
||||
*
|
||||
* @param string $action
|
||||
* @param Request $request
|
||||
* @return type
|
||||
*/
|
||||
protected function indexApiAction($action, Request $request, $_format)
|
||||
{
|
||||
$this->onPreIndex($action, $request, $_format);
|
||||
|
||||
$response = $this->checkACL($action, $request, $_format);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (!isset($entity)) {
|
||||
$entity = '';
|
||||
}
|
||||
|
||||
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$totalItems = $this->countEntities($action, $request, $_format);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$response = $this->onPreIndexBuildQuery($action, $request, $_format, $totalItems,
|
||||
$paginator);
|
||||
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$query = $this->queryEntities($action, $request, $_format, $paginator);
|
||||
|
||||
$response = $this->onPostIndexBuildQuery($action, $request, $_format, $totalItems,
|
||||
$paginator, $query);
|
||||
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$entities = $this->getQueryResult($action, $request, $_format, $totalItems, $paginator, $query);
|
||||
|
||||
$response = $this->onPostIndexFetchQuery($action, $request, $_format, $totalItems,
|
||||
$paginator, $entities);
|
||||
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->serializeCollection($action, $request, $_format, $paginator, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize collections
|
||||
*
|
||||
*/
|
||||
protected function serializeCollection(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response
|
||||
{
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
$context = $this->getContextForSerialization($action, $request, $_format, $entities);
|
||||
|
||||
return $this->json($model, Response::HTTP_OK, [], $context);
|
||||
}
|
||||
|
||||
|
||||
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the role given from the config.
|
||||
*/
|
||||
protected function getRoleFor(string $action, Request $request, $entity, $_format): string
|
||||
{
|
||||
$actionConfig = $this->getActionConfig($action);
|
||||
|
||||
if (NULL !== $actionConfig['roles'][$request->getMethod()]) {
|
||||
return $actionConfig['roles'][$request->getMethod()];
|
||||
}
|
||||
|
||||
if ($this->crudConfig['base_role']) {
|
||||
return $this->crudConfig['base_role'];
|
||||
}
|
||||
|
||||
throw new \RuntimeException(sprintf("the config does not have any role for the ".
|
||||
"method %s nor a global role for the whole action. Add those to your ".
|
||||
"configuration or override the required method", $request->getMethod()));
|
||||
|
||||
}
|
||||
|
||||
protected function getSerializer(): SerializerInterface
|
||||
{
|
||||
return $this->get('serializer');
|
||||
}
|
||||
}
|
@@ -34,6 +34,7 @@ use Chill\MainBundle\CRUD\Form\CRUDDeleteEntityForm;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Class CRUDController
|
||||
@@ -484,7 +485,7 @@ class CRUDController extends AbstractController
|
||||
* @param mixed $id
|
||||
* @return Response
|
||||
*/
|
||||
protected function viewAction(string $action, Request $request, $id)
|
||||
protected function viewAction(string $action, Request $request, $id, $_format = 'html'): Response
|
||||
{
|
||||
$entity = $this->getEntity($action, $id, $request);
|
||||
|
||||
@@ -496,7 +497,7 @@ class CRUDController extends AbstractController
|
||||
|
||||
if (NULL === $entity) {
|
||||
throw $this->createNotFoundException(sprintf("The %s with id %s "
|
||||
. "is not found"), $this->getCrudName(), $id);
|
||||
. "is not found", $this->getCrudName(), $id));
|
||||
}
|
||||
|
||||
$response = $this->checkACL($action, $entity);
|
||||
@@ -508,17 +509,36 @@ class CRUDController extends AbstractController
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$defaultTemplateParameters = [
|
||||
'entity' => $entity,
|
||||
'crud_name' => $this->getCrudName()
|
||||
];
|
||||
|
||||
return $this->render(
|
||||
$this->getTemplateFor($action, $entity, $request),
|
||||
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
|
||||
|
||||
if ($_format === 'html') {
|
||||
$defaultTemplateParameters = [
|
||||
'entity' => $entity,
|
||||
'crud_name' => $this->getCrudName()
|
||||
];
|
||||
|
||||
return $this->render(
|
||||
$this->getTemplateFor($action, $entity, $request),
|
||||
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
|
||||
);
|
||||
} elseif ($_format === 'json') {
|
||||
$context = $this->getContextForSerialization($action, $request, $entity, $_format);
|
||||
|
||||
return $this->json($entity, Response::HTTP_OK, [], $context);
|
||||
} else {
|
||||
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context for the serialization
|
||||
*/
|
||||
public function getContextForSerialization(string $action, Request $request, $entity, string $_format): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The edit action.
|
||||
@@ -799,7 +819,7 @@ class CRUDController extends AbstractController
|
||||
*/
|
||||
protected function getRoleFor($action)
|
||||
{
|
||||
if (NULL !== ($this->getActionConfig($action)['role'])) {
|
||||
if (\array_key_exists('role', $this->getActionConfig($action))) {
|
||||
return $this->getActionConfig($action)['role'];
|
||||
}
|
||||
|
||||
@@ -1181,6 +1201,7 @@ class CRUDController extends AbstractController
|
||||
AuthorizationHelper::class => AuthorizationHelper::class,
|
||||
EventDispatcherInterface::class => EventDispatcherInterface::class,
|
||||
Resolver::class => Resolver::class,
|
||||
SerializerInterface::class => SerializerInterface::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@@ -23,6 +23,9 @@ namespace Chill\MainBundle\CRUD\Routing;
|
||||
use Symfony\Component\Config\Loader\Loader;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
|
||||
/**
|
||||
* Class CRUDRoutesLoader
|
||||
@@ -32,24 +35,34 @@ use Symfony\Component\Routing\RouteCollection;
|
||||
*/
|
||||
class CRUDRoutesLoader extends Loader
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
protected array $crudConfig = [];
|
||||
|
||||
protected array $apiCrudConfig = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isLoaded = false;
|
||||
|
||||
private const ALL_SINGLE_METHODS = [
|
||||
Request::METHOD_GET,
|
||||
Request::METHOD_POST,
|
||||
Request::METHOD_PUT,
|
||||
Request::METHOD_DELETE
|
||||
];
|
||||
|
||||
private const ALL_INDEX_METHODS = [ Request::METHOD_GET, Request::METHOD_HEAD ];
|
||||
|
||||
/**
|
||||
* CRUDRoutesLoader constructor.
|
||||
*
|
||||
* @param $config
|
||||
* @param $crudConfig the config from cruds
|
||||
* @param $apicrudConfig the config from api_crud
|
||||
*/
|
||||
public function __construct($config)
|
||||
public function __construct(array $crudConfig, array $apiConfig)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->crudConfig = $crudConfig;
|
||||
$this->apiConfig = $apiConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,53 +76,161 @@ class CRUDRoutesLoader extends Loader
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RouteCollection
|
||||
* Load routes for CRUD and CRUD Api
|
||||
*/
|
||||
public function load($resource, $type = null)
|
||||
public function load($resource, $type = null): RouteCollection
|
||||
{
|
||||
|
||||
if (true === $this->isLoaded) {
|
||||
throw new \RuntimeException('Do not add the "CRUD" loader twice');
|
||||
}
|
||||
|
||||
|
||||
$collection = new RouteCollection();
|
||||
|
||||
foreach ($this->config as $config) {
|
||||
$collection->addCollection($this->loadConfig($config));
|
||||
foreach ($this->crudConfig as $crudConfig) {
|
||||
$collection->addCollection($this->loadCrudConfig($crudConfig));
|
||||
}
|
||||
|
||||
foreach ($this->apiConfig as $crudConfig) {
|
||||
$collection->addCollection($this->loadApi($crudConfig));
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $config
|
||||
* Load routes for CRUD (without api)
|
||||
*
|
||||
* @param $crudConfig
|
||||
* @return RouteCollection
|
||||
*/
|
||||
protected function loadConfig($config): RouteCollection
|
||||
protected function loadCrudConfig($crudConfig): RouteCollection
|
||||
{
|
||||
$collection = new RouteCollection();
|
||||
foreach ($config['actions'] as $name => $action) {
|
||||
$controller ='cscrud_'.$crudConfig['name'].'_controller';
|
||||
|
||||
foreach ($crudConfig['actions'] as $name => $action) {
|
||||
// defaults (controller name)
|
||||
$defaults = [
|
||||
'_controller' => 'cscrud_'.$config['name'].'_controller'.':'.($action['controller_action'] ?? $name)
|
||||
'_controller' => $controller.':'.($action['controller_action'] ?? $name)
|
||||
];
|
||||
|
||||
if ($name === 'index') {
|
||||
$path = "{_locale}".$config['base_path'];
|
||||
$path = "{_locale}".$crudConfig['base_path'];
|
||||
$route = new Route($path, $defaults);
|
||||
} elseif ($name === 'new') {
|
||||
$path = "{_locale}".$config['base_path'].'/'.$name;
|
||||
$path = "{_locale}".$crudConfig['base_path'].'/'.$name;
|
||||
$route = new Route($path, $defaults);
|
||||
} else {
|
||||
$path = "{_locale}".$config['base_path'].($action['path'] ?? '/{id}/'.$name);
|
||||
$path = "{_locale}".$crudConfig['base_path'].($action['path'] ?? '/{id}/'.$name);
|
||||
$requirements = $action['requirements'] ?? [
|
||||
'{id}' => '\d+'
|
||||
];
|
||||
$route = new Route($path, $defaults, $requirements);
|
||||
}
|
||||
|
||||
$collection->add('chill_crud_'.$config['name'].'_'.$name, $route);
|
||||
$collection->add('chill_crud_'.$crudConfig['name'].'_'.$name, $route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load routes for api single
|
||||
*
|
||||
* @param $crudConfig
|
||||
* @return RouteCollection
|
||||
*/
|
||||
protected function loadApi(array $crudConfig): RouteCollection
|
||||
{
|
||||
$collection = new RouteCollection();
|
||||
$controller ='csapi_'.$crudConfig['name'].'_controller';
|
||||
|
||||
foreach ($crudConfig['actions'] as $name => $action) {
|
||||
// filter only on single actions
|
||||
$singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL;
|
||||
if ('collection' === $singleCollection) {
|
||||
// continue;
|
||||
}
|
||||
|
||||
// compute default action
|
||||
switch ($name) {
|
||||
case '_entity':
|
||||
$controllerAction = 'entityApi';
|
||||
break;
|
||||
case '_index':
|
||||
$controllerAction = 'indexApi';
|
||||
break;
|
||||
default:
|
||||
$controllerAction = $name.'Api';
|
||||
break;
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'_controller' => $controller.':'.($action['controller_action'] ?? $controllerAction)
|
||||
];
|
||||
|
||||
// path are rewritten
|
||||
// if name === 'default', we rewrite it to nothing :-)
|
||||
$localName = \in_array($name, [ '_entity', '_index' ]) ? '' : '/'.$name;
|
||||
if ('collection' === $action['single-collection'] || '_index' === $name) {
|
||||
$localPath = $action['path'] ?? $localName.'.{_format}';
|
||||
} else {
|
||||
$localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}';
|
||||
}
|
||||
$path = $crudConfig['base_path'].$localPath;
|
||||
|
||||
$requirements = $action['requirements'] ?? [ '{id}' => '\d+' ];
|
||||
|
||||
$methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; },
|
||||
ARRAY_FILTER_USE_BOTH));
|
||||
|
||||
$route = new Route($path, $defaults, $requirements);
|
||||
$route->setMethods($methods);
|
||||
|
||||
$collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load routes for api multi
|
||||
*
|
||||
* @param $crudConfig
|
||||
* @return RouteCollection
|
||||
*/
|
||||
protected function loadApiMultiConfig(array $crudConfig): RouteCollection
|
||||
{
|
||||
$collection = new RouteCollection();
|
||||
$controller ='csapi_'.$crudConfig['name'].'_controller';
|
||||
|
||||
foreach ($crudConfig['actions'] as $name => $action) {
|
||||
// filter only on single actions
|
||||
$singleCollection = $action['single-collection'] ?? $name === '_index' ? 'collection' : NULL;
|
||||
if ('single' === $singleCollection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'_controller' => $controller.':'.($action['controller_action'] ?? '_entity' === $name ? 'entityApi' : $name.'Api')
|
||||
];
|
||||
|
||||
// path are rewritten
|
||||
// if name === 'default', we rewrite it to nothing :-)
|
||||
$localName = '_entity' === $name ? '' : '/'.$name;
|
||||
$localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}';
|
||||
$path = $crudConfig['base_path'].$localPath;
|
||||
|
||||
$requirements = $action['requirements'] ?? [ '{id}' => '\d+' ];
|
||||
|
||||
$methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; },
|
||||
ARRAY_FILTER_USE_BOTH));
|
||||
|
||||
$route = new Route($path, $defaults, $requirements);
|
||||
$route->setMethods($methods);
|
||||
|
||||
$collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompile
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass;
|
||||
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
||||
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
|
||||
|
||||
|
||||
@@ -33,5 +34,6 @@ class ChillMainBundle extends Bundle
|
||||
$container->addCompilerPass(new ACLFlagsCompilerPass());
|
||||
$container->addCompilerPass(new GroupingCenterCompilerPass());
|
||||
$container->addCompilerPass(new RenderEntityCompilerPass());
|
||||
$container->addCompilerPass(new CRUDControllerCompilerPass());
|
||||
}
|
||||
}
|
||||
|
63
src/Bundle/ChillMainBundle/Controller/AddressController.php
Normal file
63
src/Bundle/ChillMainBundle/Controller/AddressController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
|
||||
/**
|
||||
* Class AddressController
|
||||
*
|
||||
* @package Chill\MainBundle\Controller
|
||||
*/
|
||||
class AddressController extends AbstractController
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* Get API Data for showing endpoint
|
||||
*
|
||||
* @Route(
|
||||
* "/{_locale}/main/api/1.0/address/{address_id}/show.{_format}",
|
||||
* name="chill_main_address_api_show"
|
||||
* )
|
||||
* @ParamConverter("address", options={"id": "address_id"})
|
||||
*/
|
||||
public function showAddress(Address $address, $_format): Response
|
||||
{
|
||||
// TODO check ACL ?
|
||||
switch ($_format) {
|
||||
case 'json':
|
||||
return $this->json($address);
|
||||
default:
|
||||
throw new BadRequestException('Unsupported format');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get API Data for showing endpoint
|
||||
*
|
||||
* @Route(
|
||||
* "/{_locale}/main/api/1.0/address-reference/{address_reference_id}/show.{_format}",
|
||||
* name="chill_main_address_reference_api_show"
|
||||
* )
|
||||
* @ParamConverter("addressReference", options={"id": "address_reference_id"})
|
||||
*/
|
||||
public function showAddressReference(AddressReference $addressReference, $_format): Response
|
||||
{
|
||||
// TODO check ACL ?
|
||||
switch ($_format) {
|
||||
case 'json':
|
||||
return $this->json($addressReference);
|
||||
default:
|
||||
throw new BadRequestException('Unsupported format');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
|
||||
/**
|
||||
* Load reference addresses into database
|
||||
*
|
||||
* @author Champs Libres
|
||||
*/
|
||||
class LoadAddressReferences extends AbstractFixture implements ContainerAwareInterface, OrderedFixtureInterface {
|
||||
|
||||
|
||||
protected $faker;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->faker = \Faker\Factory::create('fr_FR');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $container;
|
||||
|
||||
public function setContainer(ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function getOrder() {
|
||||
return 51;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a random point
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
private function getRandomPoint()
|
||||
{
|
||||
$lonBrussels = 4.35243;
|
||||
$latBrussels = 50.84676;
|
||||
$lon = $lonBrussels + 0.01 * rand(-5, 5);
|
||||
$lat = $latBrussels + 0.01 * rand(-5, 5);
|
||||
return Point::fromLonLat($lon, $lat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random reference address
|
||||
*
|
||||
* @return AddressReference
|
||||
*/
|
||||
private function getRandomAddressReference()
|
||||
{
|
||||
$ar= new AddressReference();
|
||||
|
||||
$ar->setRefId($this->faker->numerify('ref-id-######'));
|
||||
$ar->setStreet($this->faker->streetName);
|
||||
$ar->setStreetNumber(rand(0,199));
|
||||
$ar ->setPoint($this->getRandomPoint());
|
||||
$ar->setPostcode($this->getReference(
|
||||
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)]
|
||||
));
|
||||
|
||||
$ar->setMunicipalityCode($ar->getPostcode()->getCode());
|
||||
|
||||
return $ar
|
||||
;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager) {
|
||||
|
||||
echo "loading some reference address... \n";
|
||||
|
||||
for ($i=0; $i<10; $i++) {
|
||||
$ar = $this->getRandomAddressReference();
|
||||
$manager->persist($ar);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -133,7 +133,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
|
||||
$this->configureCruds($container, $config['cruds'], $loader);
|
||||
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +188,12 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
||||
$container->prependExtensionConfig('doctrine', array(
|
||||
'dbal' => [
|
||||
'types' => [
|
||||
'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class
|
||||
'dateinterval' => [
|
||||
'class' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class
|
||||
],
|
||||
'point' => [
|
||||
'class' => \Chill\MainBundle\Doctrine\Type\PointType::class
|
||||
]
|
||||
]
|
||||
]
|
||||
));
|
||||
@@ -210,51 +215,24 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @param array $config the config under 'cruds' key
|
||||
* @return null
|
||||
* Load parameter for configuration and set parameters for api
|
||||
*/
|
||||
protected function configureCruds(ContainerBuilder $container, $config, Loader\YamlFileLoader $loader)
|
||||
protected function configureCruds(
|
||||
ContainerBuilder $container,
|
||||
array $crudConfig,
|
||||
array $apiConfig,
|
||||
Loader\YamlFileLoader $loader
|
||||
): void
|
||||
{
|
||||
if (count($config) === 0) {
|
||||
if (count($crudConfig) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loader->load('services/crud.yaml');
|
||||
|
||||
$container->setParameter('chill_main_crud_route_loader_config', $config);
|
||||
$container->setParameter('chill_main_crud_route_loader_config', $crudConfig);
|
||||
$container->setParameter('chill_main_api_route_loader_config', $apiConfig);
|
||||
|
||||
$definition = new Definition();
|
||||
$definition
|
||||
->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class)
|
||||
->addArgument('%chill_main_crud_route_loader_config%')
|
||||
;
|
||||
|
||||
$container->setDefinition('chill_main_crud_route_loader', $definition);
|
||||
|
||||
$alreadyExistingNames = [];
|
||||
|
||||
foreach ($config as $crudEntry) {
|
||||
$controller = $crudEntry['controller'];
|
||||
$controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller';
|
||||
$name = $crudEntry['name'];
|
||||
|
||||
// check for existing crud names
|
||||
if (\in_array($name, $alreadyExistingNames)) {
|
||||
throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name));
|
||||
}
|
||||
|
||||
if (!$container->has($controllerServiceName)) {
|
||||
$controllerDefinition = new Definition($controller);
|
||||
$controllerDefinition->addTag('controller.service_arguments');
|
||||
$controllerDefinition->setAutoconfigured(true);
|
||||
$controllerDefinition->setClass($crudEntry['controller']);
|
||||
$container->setDefinition($controllerServiceName, $controllerDefinition);
|
||||
}
|
||||
|
||||
$container->setParameter('chill_main_crud_config_'.$name, $crudEntry);
|
||||
$container->getDefinition($controllerServiceName)
|
||||
->addMethodCall('setCrudConfig', ['%chill_main_crud_config_'.$name.'%']);
|
||||
}
|
||||
// Note: the controller are loaded inside compiler pass
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
||||
/**
|
||||
@@ -140,7 +141,7 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('controller_action')
|
||||
->defaultNull()
|
||||
->info('the method name to call in the route. Will be set to the action name if left empty.')
|
||||
->example("'action'")
|
||||
->example("action")
|
||||
->end()
|
||||
->scalarNode('path')
|
||||
->defaultNull()
|
||||
@@ -168,6 +169,78 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
|
||||
->end()
|
||||
|
||||
|
||||
->arrayNode('apis')
|
||||
->defaultValue([])
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('class')->cannotBeEmpty()->isRequired()->end()
|
||||
->scalarNode('controller')
|
||||
->cannotBeEmpty()
|
||||
->defaultValue(\Chill\MainBundle\CRUD\Controller\ApiController::class)
|
||||
->end()
|
||||
->scalarNode('name')->cannotBeEmpty()->isRequired()->end()
|
||||
->scalarNode('base_path')->cannotBeEmpty()->isRequired()->end()
|
||||
->scalarNode('base_role')->defaultNull()->end()
|
||||
->arrayNode('actions')
|
||||
->useAttributeAsKey('name')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('controller_action')
|
||||
->defaultNull()
|
||||
->info('the method name to call in the controller. Will be set to the concatenation '.
|
||||
'of action name + \'Api\' if left empty.')
|
||||
->example("showApi")
|
||||
->end()
|
||||
->scalarNode('path')
|
||||
->defaultNull()
|
||||
->info('the path that will be **appended** after the base path. Do not forget to add ' .
|
||||
'arguments for the method. By default, will set to the action name, including an `{id}` '.
|
||||
'parameter. A suffix of action name will be appended, except if the action name '.
|
||||
'is "_entity".')
|
||||
->example('/{id}/my-action')
|
||||
->end()
|
||||
->arrayNode('requirements')
|
||||
->ignoreExtraKeys(false)
|
||||
->info('the requirements for the route. Will be set to `[ \'id\' => \'\d+\' ]` if left empty.')
|
||||
->end()
|
||||
->enumNode('single-collection')
|
||||
->values(['single', 'collection'])
|
||||
->defaultValue('single')
|
||||
->info('indicates if the returned object is a single element or a collection. '.
|
||||
'If the action name is `_index`, this value will always be considered as '.
|
||||
'`collection`')
|
||||
->end()
|
||||
->arrayNode('methods')
|
||||
->addDefaultsIfNotSet()
|
||||
->info('the allowed methods')
|
||||
->children()
|
||||
->booleanNode(Request::METHOD_GET)->defaultTrue()->end()
|
||||
->booleanNode(Request::METHOD_HEAD)->defaultTrue()->end()
|
||||
->booleanNode(Request::METHOD_POST)->defaultFalse()->end()
|
||||
->booleanNode(Request::METHOD_DELETE)->defaultFalse()->end()
|
||||
->booleanNode(Request::METHOD_PUT)->defaultFalse()->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('roles')
|
||||
->addDefaultsIfNotSet()
|
||||
->info("The role require for each http method")
|
||||
->children()
|
||||
->scalarNode(Request::METHOD_GET)->defaultNull()->end()
|
||||
->scalarNode(Request::METHOD_HEAD)->defaultNull()->end()
|
||||
->scalarNode(Request::METHOD_POST)->defaultNull()->end()
|
||||
->scalarNode(Request::METHOD_DELETE)->defaultNull()->end()
|
||||
->scalarNode(Request::METHOD_PUT)->defaultNull()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
|
||||
->end()
|
||||
->end() // end of root/children
|
||||
->end() // end of root
|
||||
;
|
||||
|
103
src/Bundle/ChillMainBundle/Doctrine/Model/Point.php
Normal file
103
src/Bundle/ChillMainBundle/Doctrine/Model/Point.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Model;
|
||||
|
||||
use \JsonSerializable;
|
||||
|
||||
/**
|
||||
* Description of Point
|
||||
*
|
||||
*/
|
||||
class Point implements JsonSerializable {
|
||||
private float $lat;
|
||||
private float $lon;
|
||||
public static string $SRID = '4326';
|
||||
|
||||
private function __construct(float $lon, float $lat)
|
||||
{
|
||||
$this->lat = $lat;
|
||||
$this->lon = $lon;
|
||||
}
|
||||
|
||||
public function toGeoJson(): string
|
||||
{
|
||||
$array = $this->toArrayGeoJson();
|
||||
return \json_encode($array);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArrayGeoJson();
|
||||
}
|
||||
|
||||
public function toArrayGeoJson(): array
|
||||
{
|
||||
return [
|
||||
"type" => "Point",
|
||||
"coordinates" => [ $this->lon, $this->lat ]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toWKT(): string
|
||||
{
|
||||
return 'SRID='.self::$SRID.';POINT('.$this->lon.' '.$this->lat.')';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type $geojson
|
||||
* @return Point
|
||||
*/
|
||||
public static function fromGeoJson(string $geojson): Point
|
||||
{
|
||||
$a = json_decode($geojson);
|
||||
//check if the geojson string is correct
|
||||
if (NULL === $a or !isset($a->type) or !isset($a->coordinates)){
|
||||
throw PointException::badJsonString($geojson);
|
||||
}
|
||||
|
||||
if ($a->type != 'Point'){
|
||||
throw PointException::badGeoType();
|
||||
}
|
||||
|
||||
$lat = $a->coordinates[1];
|
||||
$lon = $a->coordinates[0];
|
||||
|
||||
return Point::fromLonLat($lon, $lat);
|
||||
}
|
||||
|
||||
public static function fromLonLat(float $lon, float $lat): Point
|
||||
{
|
||||
if (($lon > -180 && $lon < 180) && ($lat > -90 && $lat < 90))
|
||||
{
|
||||
return new Point($lon, $lat);
|
||||
} else {
|
||||
throw PointException::badCoordinates($lon, $lat);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromArrayGeoJson(array $array): Point
|
||||
{
|
||||
if ($array['type'] == 'Point' &&
|
||||
isset($array['coordinates']))
|
||||
{
|
||||
return self::fromLonLat($array['coordinates'][0], $array['coordinates'][1]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLat(): float
|
||||
{
|
||||
return $this->lat;
|
||||
}
|
||||
|
||||
public function getLon(): float
|
||||
{
|
||||
return $this->lon;
|
||||
}
|
||||
}
|
||||
|
||||
|
27
src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php
Normal file
27
src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Model;
|
||||
|
||||
use \Exception;
|
||||
|
||||
/**
|
||||
* Description of PointException
|
||||
*
|
||||
*/
|
||||
class PointException extends Exception {
|
||||
|
||||
public static function badCoordinates($lon, $lat): self
|
||||
{
|
||||
return new self("Input coordinates are not valid in the used coordinate system (longitude = $lon , latitude = $lat)");
|
||||
}
|
||||
|
||||
public static function badJsonString($str): self
|
||||
{
|
||||
return new self("The JSON string is not valid: $str");
|
||||
}
|
||||
|
||||
public static function badGeoType(): self
|
||||
{
|
||||
return new self("The geoJSON object type is not valid");
|
||||
}
|
||||
}
|
75
src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php
Normal file
75
src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Type;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Chill\MainBundle\Doctrine\Model\PointException;
|
||||
|
||||
|
||||
/**
|
||||
* A Type for Doctrine to implement the Geography Point type
|
||||
* implemented by Postgis on postgis+postgresql databases
|
||||
*
|
||||
*/
|
||||
class PointType extends Type {
|
||||
|
||||
const POINT = 'point';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $fieldDeclaration
|
||||
* @param AbstractPlatform $platform
|
||||
* @return type
|
||||
*/
|
||||
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
{
|
||||
return 'geometry(POINT,'.Point::$SRID.')';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type $value
|
||||
* @param AbstractPlatform $platform
|
||||
* @return Point
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
if ($value === NULL){
|
||||
return NULL;
|
||||
} else {
|
||||
return Point::fromGeoJson($value);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return self::POINT;
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
if ($value === NULL){
|
||||
return NULL;
|
||||
} else {
|
||||
return $value->toWKT();
|
||||
}
|
||||
}
|
||||
|
||||
public function canRequireSQLConversion()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function convertToPHPValueSQL($sqlExpr, $platform)
|
||||
{
|
||||
return 'ST_AsGeoJSON('.$sqlExpr.') ';
|
||||
}
|
||||
|
||||
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
|
||||
{
|
||||
return $sqlExpr;
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
|
||||
/**
|
||||
* Address
|
||||
@@ -28,14 +30,14 @@ class Address
|
||||
*
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $streetAddress1 = '';
|
||||
private $street = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $streetAddress2 = '';
|
||||
private $streetNumber = '';
|
||||
|
||||
/**
|
||||
* @var PostalCode
|
||||
@@ -43,7 +45,56 @@ class Address
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
|
||||
*/
|
||||
private $postcode;
|
||||
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=16, nullable=true)
|
||||
*/
|
||||
private $floor;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=16, nullable=true)
|
||||
*/
|
||||
private $corridor;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=16, nullable=true)
|
||||
*/
|
||||
private $steps;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $buildingName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=16, nullable=true)
|
||||
*/
|
||||
private $flat;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $distribution;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $extra;
|
||||
|
||||
/**
|
||||
* Indicates when the address starts validation. Used to build an history
|
||||
* of address. By default, the current date.
|
||||
@@ -53,27 +104,55 @@ class Address
|
||||
* @ORM\Column(type="date")
|
||||
*/
|
||||
private $validFrom;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates when the address ends. Used to build an history
|
||||
* of address.
|
||||
*
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @ORM\Column(type="date", nullable=true)
|
||||
*/
|
||||
private $validTo;
|
||||
|
||||
/**
|
||||
* True if the address is a "no address", aka homeless person, ...
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isNoAddress = false;
|
||||
|
||||
|
||||
/**
|
||||
* A geospatial field storing the coordinates of the Address
|
||||
*
|
||||
* @var Point|null
|
||||
*
|
||||
* @ORM\Column(type="point", nullable=true)
|
||||
*/
|
||||
private $point;
|
||||
|
||||
/**
|
||||
* A ThirdParty reference for person's addresses that are linked to a third party
|
||||
*
|
||||
* @var ThirdParty|null
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private $linkedToThirdParty;
|
||||
|
||||
/**
|
||||
* A list of metadata, added by customizable fields
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $customs = [];
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->validFrom = new \DateTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get id
|
||||
*
|
||||
@@ -85,7 +164,7 @@ class Address
|
||||
}
|
||||
|
||||
/**
|
||||
* Set streetAddress1
|
||||
* Set streetAddress1 (legacy function)
|
||||
*
|
||||
* @param string $streetAddress1
|
||||
*
|
||||
@@ -93,23 +172,23 @@ class Address
|
||||
*/
|
||||
public function setStreetAddress1($streetAddress1)
|
||||
{
|
||||
$this->streetAddress1 = $streetAddress1 === NULL ? '' : $streetAddress1;
|
||||
$this->street = $streetAddress1 === NULL ? '' : $streetAddress1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streetAddress1
|
||||
* Get streetAddress1 (legacy function)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStreetAddress1()
|
||||
{
|
||||
return $this->streetAddress1;
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set streetAddress2
|
||||
* Set streetAddress2 (legacy function)
|
||||
*
|
||||
* @param string $streetAddress2
|
||||
*
|
||||
@@ -117,19 +196,19 @@ class Address
|
||||
*/
|
||||
public function setStreetAddress2($streetAddress2)
|
||||
{
|
||||
$this->streetAddress2 = $streetAddress2 === NULL ? '' : $streetAddress2;
|
||||
$this->streetNumber = $streetAddress2 === NULL ? '' : $streetAddress2;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streetAddress2
|
||||
* Get streetAddress2 (legacy function)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStreetAddress2()
|
||||
{
|
||||
return $this->streetAddress2;
|
||||
return $this->streetNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +234,7 @@ class Address
|
||||
{
|
||||
return $this->postcode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
@@ -173,19 +252,19 @@ class Address
|
||||
$this->validFrom = $validFrom;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get IsNoAddress
|
||||
*
|
||||
*
|
||||
* Indicate true if the address is a fake address (homeless, ...)
|
||||
*
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsNoAddress(): bool
|
||||
{
|
||||
return $this->isNoAddress;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
@@ -196,9 +275,9 @@ class Address
|
||||
|
||||
/**
|
||||
* Set IsNoAddress
|
||||
*
|
||||
*
|
||||
* Indicate true if the address is a fake address (homeless, ...)
|
||||
*
|
||||
*
|
||||
* @param bool $isNoAddress
|
||||
* @return $this
|
||||
*/
|
||||
@@ -207,10 +286,10 @@ class Address
|
||||
$this->isNoAddress = $isNoAddress;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get customs informations in the address
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCustoms(): array
|
||||
@@ -220,27 +299,27 @@ class Address
|
||||
|
||||
/**
|
||||
* Store custom informations in the address
|
||||
*
|
||||
*
|
||||
* @param array $customs
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustoms(array $customs): self
|
||||
{
|
||||
$this->customs = $customs;
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate the address.
|
||||
*
|
||||
*
|
||||
* Check that:
|
||||
*
|
||||
*
|
||||
* * if the address is not home address:
|
||||
* * the postal code is present
|
||||
* * the valid from is not null
|
||||
* * the address street 1 is greater than 2
|
||||
*
|
||||
*
|
||||
* @param ExecutionContextInterface $context
|
||||
* @param array $payload
|
||||
*/
|
||||
@@ -252,18 +331,18 @@ class Address
|
||||
->atPath('validFrom')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
|
||||
if ($this->isNoAddress()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (empty($this->getStreetAddress1())) {
|
||||
$context
|
||||
->buildViolation("address.street1-should-be-set")
|
||||
->atPath('streetAddress1')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
|
||||
if (!$this->getPostcode() instanceof PostalCode) {
|
||||
$context
|
||||
->buildViolation("address.postcode-should-be-set")
|
||||
@@ -271,7 +350,7 @@ class Address
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Address $original
|
||||
* @return Address
|
||||
@@ -286,5 +365,149 @@ class Address
|
||||
;
|
||||
}
|
||||
|
||||
public function getStreet(): ?string
|
||||
{
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
public function setStreet(string $street): self
|
||||
{
|
||||
$this->street = $street;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreetNumber(): ?string
|
||||
{
|
||||
return $this->streetNumber;
|
||||
}
|
||||
|
||||
public function setStreetNumber(string $streetNumber): self
|
||||
{
|
||||
$this->streetNumber = $streetNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFloor(): ?string
|
||||
{
|
||||
return $this->floor;
|
||||
}
|
||||
|
||||
public function setFloor(?string $floor): self
|
||||
{
|
||||
$this->floor = $floor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCorridor(): ?string
|
||||
{
|
||||
return $this->corridor;
|
||||
}
|
||||
|
||||
public function setCorridor(?string $corridor): self
|
||||
{
|
||||
$this->corridor = $corridor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSteps(): ?string
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
public function setSteps(?string $steps): self
|
||||
{
|
||||
$this->steps = $steps;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuildingName(): ?string
|
||||
{
|
||||
return $this->buildingName;
|
||||
}
|
||||
|
||||
public function setBuildingName(?string $buildingName): self
|
||||
{
|
||||
$this->buildingName = $buildingName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFlat(): ?string
|
||||
{
|
||||
return $this->flat;
|
||||
}
|
||||
|
||||
public function setFlat(?string $flat): self
|
||||
{
|
||||
$this->flat = $flat;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDistribution(): ?string
|
||||
{
|
||||
return $this->distribution;
|
||||
}
|
||||
|
||||
public function setDistribution(?string $distribution): self
|
||||
{
|
||||
$this->distribution = $distribution;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExtra(): ?string
|
||||
{
|
||||
return $this->extra;
|
||||
}
|
||||
|
||||
public function setExtra(?string $extra): self
|
||||
{
|
||||
$this->extra = $extra;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidTo(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->validTo;
|
||||
}
|
||||
|
||||
public function setValidTo(\DateTimeInterface $validTo): self
|
||||
{
|
||||
$this->validTo = $validTo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPoint(): ?Point
|
||||
{
|
||||
return $this->point;
|
||||
}
|
||||
|
||||
public function setPoint(?Point $point): self
|
||||
{
|
||||
$this->point = $point;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLinkedToThirdParty()
|
||||
{
|
||||
return $this->linkedToThirdParty;
|
||||
}
|
||||
|
||||
public function setLinkedToThirdParty($linkedToThirdParty): self
|
||||
{
|
||||
$this->linkedToThirdParty = $linkedToThirdParty;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
165
src/Bundle/ChillMainBundle/Entity/AddressReference.php
Normal file
165
src/Bundle/ChillMainBundle/Entity/AddressReference.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\AddressReferenceRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=AddressReferenceRepository::class)
|
||||
* @ORM\Table(name="chill_main_address_reference")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class AddressReference
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $refId;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $street;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $streetNumber;
|
||||
|
||||
/**
|
||||
* @var PostalCode
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
|
||||
*/
|
||||
private $postcode;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $municipalityCode;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* A geospatial field storing the coordinates of the Address
|
||||
*
|
||||
* @var Point
|
||||
*
|
||||
* @ORM\Column(type="point")
|
||||
*/
|
||||
private $point;
|
||||
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRefId(): ?string
|
||||
{
|
||||
return $this->refId;
|
||||
}
|
||||
|
||||
public function setRefId(string $refId): self
|
||||
{
|
||||
$this->refId = $refId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreet(): ?string
|
||||
{
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
public function setStreet(?string $street): self
|
||||
{
|
||||
$this->street = $street;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreetNumber(): ?string
|
||||
{
|
||||
return $this->streetNumber;
|
||||
}
|
||||
|
||||
public function setStreetNumber(?string $streetNumber): self
|
||||
{
|
||||
$this->streetNumber = $streetNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set postcode
|
||||
*
|
||||
* @param PostalCode $postcode
|
||||
*
|
||||
* @return Address
|
||||
*/
|
||||
public function setPostcode(PostalCode $postcode = null)
|
||||
{
|
||||
$this->postcode = $postcode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get postcode
|
||||
*
|
||||
* @return PostalCode
|
||||
*/
|
||||
public function getPostcode()
|
||||
{
|
||||
return $this->postcode;
|
||||
}
|
||||
|
||||
public function getMunicipalityCode(): ?string
|
||||
{
|
||||
return $this->municipalityCode;
|
||||
}
|
||||
|
||||
public function setMunicipalityCode(?string $municipalityCode): self
|
||||
{
|
||||
$this->municipalityCode = $municipalityCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSource(): ?string
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setSource(?string $source): self
|
||||
{
|
||||
$this->source = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPoint(): ?Point
|
||||
{
|
||||
return $this->point;
|
||||
}
|
||||
|
||||
public function setPoint(?Point $point): self
|
||||
{
|
||||
$this->point = $point;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -33,8 +33,8 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
* A type to create/update Address entity
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `has_valid_from` (boolean): show if an entry "has valid from" must be
|
||||
*
|
||||
* - `has_valid_from` (boolean): show if an entry "has valid from" must be
|
||||
* shown.
|
||||
* - `null_if_empty` (boolean): replace the address type by null if the street
|
||||
* or the postCode is empty. This is useful when the address is not required and
|
||||
@@ -45,10 +45,10 @@ class AddressType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('streetAddress1', TextType::class, array(
|
||||
->add('street', TextType::class, array(
|
||||
'required' => !$options['has_no_address'] // true if has no address is false
|
||||
))
|
||||
->add('streetAddress2', TextType::class, array(
|
||||
->add('streetNumber', TextType::class, array(
|
||||
'required' => false
|
||||
))
|
||||
->add('postCode', PostalCodeType::class, array(
|
||||
@@ -57,7 +57,7 @@ class AddressType extends AbstractType
|
||||
'required' => !$options['has_no_address'] // true if has no address is false
|
||||
))
|
||||
;
|
||||
|
||||
|
||||
if ($options['has_valid_from']) {
|
||||
$builder
|
||||
->add('validFrom', DateType::class, array(
|
||||
@@ -67,7 +67,7 @@ class AddressType extends AbstractType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($options['has_no_address']) {
|
||||
$builder
|
||||
->add('isNoAddress', ChoiceType::class, [
|
||||
@@ -79,12 +79,12 @@ class AddressType extends AbstractType
|
||||
'label' => 'address.address_homeless'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
if ($options['null_if_empty'] === TRUE) {
|
||||
$builder->setDataMapper(new AddressDataMapper());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method AddressReference|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method AddressReference|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method AddressReference[] findAll()
|
||||
* @method AddressReference[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class AddressReferenceRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AddressReference::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return AddressReference[] Returns an array of AddressReference objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('a.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?AddressReference
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
@@ -28,7 +28,7 @@
|
||||
// @import "bootstrap/scss/card";
|
||||
// @import "bootstrap/scss/breadcrumb";
|
||||
// @import "bootstrap/scss/pagination";
|
||||
// @import "bootstrap/scss/badge";
|
||||
@import "bootstrap/scss/badge";
|
||||
// @import "bootstrap/scss/jumbotron";
|
||||
// @import "bootstrap/scss/alert";
|
||||
// @import "bootstrap/scss/progress";
|
||||
@@ -41,7 +41,7 @@
|
||||
// @import "bootstrap/scss/popover";
|
||||
// @import "bootstrap/scss/carousel";
|
||||
// @import "bootstrap/scss/spinners";
|
||||
// @import "bootstrap/scss/utilities";
|
||||
@import "bootstrap/scss/utilities";
|
||||
// @import "bootstrap/scss/print";
|
||||
|
||||
@import "custom";
|
||||
|
@@ -5,7 +5,7 @@
|
||||
@include button($green, $white);
|
||||
}
|
||||
|
||||
&.bt-reset, &.bt-delete {
|
||||
&.bt-reset, &.bt-delete, &.bt-remove {
|
||||
@include button($red, $white);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
&.bt-save::before,
|
||||
&.bt-new::before,
|
||||
&.bt-delete::before,
|
||||
&.bt-remove::before,
|
||||
&.bt-update::before,
|
||||
&.bt-edit::before,
|
||||
&.bt-cancel::before,
|
||||
@@ -56,7 +57,12 @@
|
||||
// add a trash
|
||||
content: "";
|
||||
}
|
||||
|
||||
|
||||
&.bt-remove::before {
|
||||
// add a times
|
||||
content: "";
|
||||
}
|
||||
|
||||
&.bt-edit::before, &.bt-update::before {
|
||||
// add a pencil
|
||||
content: "";
|
||||
@@ -94,6 +100,7 @@
|
||||
&.bt-save::before,
|
||||
&.bt-new::before,
|
||||
&.bt-delete::before,
|
||||
&.bt-remove::before,
|
||||
&.bt-update::before,
|
||||
&.bt-edit::before,
|
||||
&.bt-cancel::before,
|
||||
@@ -123,6 +130,7 @@
|
||||
&.bt-save::before,
|
||||
&.bt-new::before,
|
||||
&.bt-delete::before,
|
||||
&.bt-remove::before,
|
||||
&.bt-update::before,
|
||||
&.bt-edit::before,
|
||||
&.bt-cancel::before,
|
||||
|
@@ -39,6 +39,8 @@ div.subheader {
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
//// VUEJS ////
|
||||
|
||||
div.vue-component {
|
||||
padding: 1.5em;
|
||||
margin: 2em 0;
|
||||
@@ -55,3 +57,97 @@ div.vue-component {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discret {
|
||||
color: grey;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<transition name="modal">
|
||||
<div class="modal-mask">
|
||||
<!-- :: styles bootstrap :: -->
|
||||
<div class="modal-dialog" :class="modalDialogClass">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<slot name="header"></slot>
|
||||
<button class="close sc-button grey" @click="$emit('close')">
|
||||
<i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="modal-body up" style="overflow-y: unset;">
|
||||
<slot name="body-fixed"></slot>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="sc-button cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :: end styles bootstrap :: -->
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/*
|
||||
* This Modal component is a mix between :
|
||||
* - Vue3 modal implementation
|
||||
* => with 'v-if:showModal' directive:parameter, html scope is added/removed not just shown/hidden
|
||||
* => with slot we can pass content from parent component
|
||||
* => some classes are passed from parent component
|
||||
* - Bootstrap 4.6 _modal.scss module
|
||||
* => using bootstrap css classes, the modal have a responsive behaviour,
|
||||
* => modal design can be configured using css classes (size, scroll)
|
||||
*/
|
||||
export default {
|
||||
name: 'Modal',
|
||||
props: ['modalDialogClass'],
|
||||
emits: ['close']
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,55 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const datetimeFormats = {
|
||||
fr: {
|
||||
short: {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric"
|
||||
},
|
||||
long: {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false
|
||||
}
|
||||
}
|
||||
};
|
||||
const messages = {
|
||||
fr: {
|
||||
action: {
|
||||
actions: "Actions",
|
||||
show: "Voir",
|
||||
edit: "Modifier",
|
||||
create: "Créer",
|
||||
remove: "Enlever",
|
||||
delete: "Supprimer",
|
||||
save: "Enregistrer",
|
||||
add: "Ajouter",
|
||||
show_modal: "Ouvrir une modale",
|
||||
ok: "OK",
|
||||
cancel: "Annuler",
|
||||
close: "Fermer",
|
||||
next: "Suivant",
|
||||
previous: "Précédent",
|
||||
back: "Retour",
|
||||
check_all: "cocher tout",
|
||||
reset: "réinitialiser"
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const _createI18n = (appMessages) => {
|
||||
Object.assign(messages.fr, appMessages.fr);
|
||||
return createI18n({
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
datetimeFormats,
|
||||
messages,
|
||||
})
|
||||
};
|
||||
|
||||
export { _createI18n }
|
@@ -6,8 +6,8 @@
|
||||
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
|
||||
{% endif %}
|
||||
<div class="chill_address_address">
|
||||
{% if address.streetAddress1 is not empty %}<p class="street street1">{{ address.streetAddress1 }}</p>{% endif %}
|
||||
{% if address.streetAddress2 is not empty %}<p class="street street2">{{ address.streetAddress2 }}</p>{% endif %}
|
||||
{% if address.street is not empty %}<p class="street street1">{{ address.street }}</p>{% endif %}
|
||||
{% if address.streetNumber is not empty %}<p class="street street2">{{ address.streetNumber }}</p>{% endif %}
|
||||
{% if address.postCode is not empty %}
|
||||
<p class="postalCode"><span class="code">{{ address.postCode.code }}</span> <span class="name">{{ address.postCode.name }}</span></p>
|
||||
<p class="country">{{ address.postCode.country.name|localize_translatable_string }}</p>
|
||||
|
28
src/Bundle/ChillMainBundle/Serializer/Model/Collection.php
Normal file
28
src/Bundle/ChillMainBundle/Serializer/Model/Collection.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Serializer\Model;
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
|
||||
class Collection
|
||||
{
|
||||
private PaginatorInterface $paginator;
|
||||
|
||||
private $items;
|
||||
|
||||
public function __construct($items, PaginatorInterface $paginator)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
public function getPaginator(): PaginatorInterface
|
||||
{
|
||||
return $this->paginator;
|
||||
}
|
||||
|
||||
public function getItems()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
|
||||
class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $data instanceof Collection;
|
||||
}
|
||||
|
||||
public function normalize($collection, string $format = null, array $context = [])
|
||||
{
|
||||
/** @var $collection Collection */
|
||||
/** @var $collection Chill\MainBundle\Pagination\PaginatorInterface */
|
||||
$paginator = $collection->getPaginator();
|
||||
|
||||
$data['count'] = $paginator->getTotalItems();
|
||||
$pagination['first'] = $paginator->getCurrentPageFirstItemNumber();
|
||||
$pagination['items_per_page'] = $paginator->getItemsPerPage();
|
||||
$pagination['next'] = $paginator->hasNextPage() ?
|
||||
$paginator->getNextPage()->generateUrl() : null;
|
||||
$pagination['previous'] = $paginator->hasPreviousPage() ?
|
||||
$paginator->getPreviousPage()->generateUrl() : null;
|
||||
$pagination['more'] = $paginator->hasNextPage();
|
||||
$data['pagination'] = $pagination;
|
||||
|
||||
// normalize results
|
||||
$data['results'] = $this->normalizer->normalize($collection->getItems(),
|
||||
$format, $context);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
119
src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php
Normal file
119
src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Tests\Doctrine\Model;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
|
||||
/**
|
||||
* Test the point model methods
|
||||
*
|
||||
* @author Julien Minet <julien.minet@champs-libres.coop>
|
||||
*/
|
||||
class ExportControllerTest extends KernelTestCase
|
||||
{
|
||||
|
||||
public function testToWKT()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);
|
||||
|
||||
$this->assertEquals($point->toWKT(),'SRID=4326;POINT(4.8634 50.47382)');
|
||||
}
|
||||
|
||||
public function testToGeojson()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);
|
||||
|
||||
$this->assertEquals($point->toGeoJson(),'{"type":"Point","coordinates":[4.8634,50.47382]}');
|
||||
}
|
||||
|
||||
public function testToArrayGeoJson()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);
|
||||
|
||||
$this->assertEquals(
|
||||
$point->toArrayGeoJson(),
|
||||
[
|
||||
'type' => 'Point',
|
||||
'coordinates' => [4.8634, 50.47382]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function testJsonSerialize()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);
|
||||
|
||||
$this->assertEquals(
|
||||
$point->jsonSerialize(),
|
||||
[
|
||||
'type' => 'Point',
|
||||
'coordinates' => [4.8634, 50.47382]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function testFromGeoJson()
|
||||
{
|
||||
$geojson = '{"type":"Point","coordinates":[4.8634,50.47382]}';
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);;
|
||||
|
||||
$this->assertEquals($point, Point::fromGeoJson($geojson));
|
||||
}
|
||||
|
||||
public function testFromLonLat()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);;
|
||||
|
||||
$this->assertEquals($point, Point::fromLonLat($lon, $lat));
|
||||
}
|
||||
|
||||
public function testFromArrayGeoJson()
|
||||
{
|
||||
$array = [
|
||||
'type' => 'Point',
|
||||
'coordinates' => [4.8634, 50.47382]
|
||||
];
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);;
|
||||
|
||||
$this->assertEquals($point, Point::fromArrayGeoJson($array));
|
||||
}
|
||||
|
||||
public function testGetLat()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);;
|
||||
|
||||
$this->assertEquals($lat, $point->getLat());
|
||||
}
|
||||
|
||||
public function testGetLon()
|
||||
{
|
||||
$lon = 4.8634;
|
||||
$lat = 50.47382;
|
||||
$point = $this->preparePoint($lon, $lat);;
|
||||
|
||||
$this->assertEquals($lon, $point->getLon());
|
||||
}
|
||||
|
||||
private function preparePoint($lon, $lat)
|
||||
{
|
||||
return Point::fromLonLat($lon, $lat);
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
services:
|
||||
Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader:
|
||||
arguments:
|
||||
$config: '%chill_main_crud_route_loader_config%'
|
||||
$crudConfig: '%chill_main_crud_route_loader_config%'
|
||||
$apiConfig: '%chill_main_api_route_loader_config%'
|
||||
tags: [ routing.loader ]
|
||||
|
||||
Chill\MainBundle\CRUD\Resolver\Resolver:
|
||||
@@ -13,4 +14,4 @@ services:
|
||||
arguments:
|
||||
$resolver: '@Chill\MainBundle\CRUD\Resolver\Resolver'
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
- { name: twig.extension }
|
||||
|
@@ -6,6 +6,7 @@ services:
|
||||
- "@request_stack"
|
||||
- "@router"
|
||||
- "%chill_main.pagination.item_per_page%"
|
||||
|
||||
Chill\MainBundle\Pagination\PaginatorFactory: '@chill_main.paginator_factory'
|
||||
|
||||
chill_main.paginator.twig_extensions:
|
||||
|
@@ -11,3 +11,7 @@ services:
|
||||
Chill\MainBundle\Serializer\Normalizer\UserNormalizer:
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 64 }
|
||||
|
||||
Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer:
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 64 }
|
||||
|
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create the postgis extension
|
||||
*/
|
||||
final class Version20210414091001 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE EXTENSION IF NOT EXISTS postgis;');
|
||||
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('DROP EXTENSION IF NOT EXISTS postgis;');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Enable the postgis extension in public schema";
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add new fields to address, including a Point geometry field.
|
||||
*/
|
||||
final class Version20210420115006 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return 'Add a Point data type and modify the Address entity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_address RENAME COLUMN streetaddress1 TO street;');
|
||||
$this->addSql('ALTER TABLE chill_main_address RENAME COLUMN streetaddress2 TO streetNumber;');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD floor VARCHAR(16) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD corridor VARCHAR(16) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD steps VARCHAR(16) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD buildingName VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD flat VARCHAR(16) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD distribution VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD extra VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD validTo DATE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD point geometry(POINT,4326) DEFAULT NULL');
|
||||
}
|
||||
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_address RENAME COLUMN street TO streetaddress1;');
|
||||
$this->addSql('ALTER TABLE chill_main_address RENAME COLUMN streetNumber TO streetaddress2;');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP floor');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP corridor');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP steps');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP buildingName');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP flat');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP distribution');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP extra');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP validTo');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP point');
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add a AddressReference table for storing authoritative address data
|
||||
*/
|
||||
final class Version20210503085107 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a AddressReference table for storing authoritative address data';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_address_reference_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_main_address_reference (id INT NOT NULL, postcode_id INT DEFAULT NULL, refId VARCHAR(255) NOT NULL, street VARCHAR(255) DEFAULT NULL, streetNumber VARCHAR(255) DEFAULT NULL, municipalityCode VARCHAR(255) DEFAULT NULL, source VARCHAR(255) DEFAULT NULL, point geometry(POINT,4326) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_CA6C1BD7EECBFDF1 ON chill_main_address_reference (postcode_id)');
|
||||
$this->addSql('ALTER TABLE chill_main_address_reference ADD CONSTRAINT FK_CA6C1BD7EECBFDF1 FOREIGN KEY (postcode_id) REFERENCES chill_main_postal_code (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_main_address_reference_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_main_address_reference');
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add linkedToThirdParty field to Address
|
||||
*/
|
||||
final class Version20210505153727 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add linkedToThirdParty field to Address';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD linkedToThirdParty_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F6114B8DD9 FOREIGN KEY (linkedToThirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_165051F6114B8DD9 ON chill_main_address (linkedToThirdParty_id)');
|
||||
$this->addSql('
|
||||
CREATE TABLE chill_main_address_legacy AS
|
||||
TABLE chill_main_address;
|
||||
');
|
||||
$this->addSql('
|
||||
WITH hydrated_addresses AS (
|
||||
SELECT *, rank() OVER (PARTITION BY pa_a.person_id ORDER BY validfrom)
|
||||
FROM chill_main_address AS aa JOIN chill_person_persons_to_addresses AS pa_a ON aa.id = pa_a.address_id
|
||||
)
|
||||
UPDATE chill_main_address AS b
|
||||
SET validto = (
|
||||
SELECT validfrom - INTERVAL \'1 DAY\'
|
||||
FROM hydrated_addresses
|
||||
WHERE hydrated_addresses.id = (
|
||||
SELECT a1.id
|
||||
FROM hydrated_addresses AS a1 JOIN hydrated_addresses AS a2 ON a2.person_id = a1.person_id AND a2.rank = (a1.rank-1)
|
||||
WHERE a2.id = b.id
|
||||
)
|
||||
);
|
||||
');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT FK_165051F6114B8DD9');
|
||||
$this->addSql('DROP INDEX IDX_165051F6114B8DD9');
|
||||
$this->addSql('ALTER TABLE chill_main_address DROP linkedToThirdParty_id');
|
||||
$this->addSql('DROP TABLE IF EXISTS chill_main_address_legacy');
|
||||
$this->addSql('
|
||||
UPDATE chill_main_address
|
||||
SET validto = null;
|
||||
');
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class AccompanyingCourseApiController extends ApiController
|
||||
{
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected ValidatorInterface $validator;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, $validator)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function participationApi($id, Request $request, $_format)
|
||||
{
|
||||
/** @var AccompanyingPeriod $accompanyingPeriod */
|
||||
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
|
||||
$person = $this->getSerializer()
|
||||
->deserialize($request->getContent(), Person::class, $_format, []);
|
||||
|
||||
if (NULL === $person) {
|
||||
throw new BadRequestException('person id not found');
|
||||
}
|
||||
|
||||
// TODO add acl
|
||||
//
|
||||
$this->onPostCheckACL('participation', $request, $_format, $accompanyingPeriod);
|
||||
|
||||
switch ($request->getMethod()) {
|
||||
case Request::METHOD_POST:
|
||||
$participation = $accompanyingPeriod->addPerson($person);
|
||||
break;
|
||||
case Request::METHOD_DELETE:
|
||||
$participation = $accompanyingPeriod->removePerson($person);
|
||||
$participation->setEndDate(new \DateTimeImmutable('now'));
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException("This method is not supported");
|
||||
}
|
||||
|
||||
$errors = $this->validator->validate($accompanyingPeriod);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
// only format accepted
|
||||
return $this->json($errors);
|
||||
}
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return $this->json($participation);
|
||||
}
|
||||
|
||||
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response
|
||||
{
|
||||
$this->eventDispatcher->dispatch(
|
||||
AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT,
|
||||
new AccompanyingPeriodPrivacyEvent($entity, [
|
||||
'action' => $action,
|
||||
'request' => $request->getMethod()
|
||||
])
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -121,7 +121,7 @@ class AccompanyingCourseController extends Controller
|
||||
* @Route(
|
||||
* "/{_locale}/person/api/1.0/accompanying-course/{accompanying_period_id}/participation.{_format}",
|
||||
* name="chill_person_accompanying_course_api_add_participation",
|
||||
* methods={"POST"},
|
||||
* methods={"POST","DELETE"},
|
||||
* format="json",
|
||||
* requirements={
|
||||
* "_format": "json",
|
||||
@@ -129,7 +129,7 @@ class AccompanyingCourseController extends Controller
|
||||
* )
|
||||
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})
|
||||
*/
|
||||
public function addParticipationAPI(Request $request, AccompanyingPeriod $accompanyingCourse, $_format): Response
|
||||
public function participationAPI(Request $request, AccompanyingPeriod $accompanyingCourse, $_format): Response
|
||||
{
|
||||
switch ($_format) {
|
||||
case 'json':
|
||||
@@ -146,7 +146,9 @@ class AccompanyingCourseController extends Controller
|
||||
}
|
||||
|
||||
// TODO add acl
|
||||
$accompanyingCourse->addPerson($person);
|
||||
$participation = ($request->getMethod() === 'POST') ?
|
||||
$accompanyingCourse->addPerson($person) : $accompanyingCourse->removePerson($person);
|
||||
|
||||
$errors = $this->validator->validate($accompanyingCourse);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
@@ -156,6 +158,6 @@ class AccompanyingCourseController extends Controller
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return new JsonResponse();
|
||||
return $this->json($participation);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class OpeningApiController extends ApiController
|
||||
{
|
||||
protected function customizeQuery(string $action, Request $request, $qb): void
|
||||
{
|
||||
$qb->where($qb->expr()->gt('e.noActiveAfter', ':now'))
|
||||
->orWhere($qb->expr()->isNull('e.noActiveAfter'));
|
||||
$qb->setParameter('now', new \DateTime('now'));
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
|
||||
/**
|
||||
* Description of LoadAccompanyingPeriod
|
||||
*
|
||||
* @author Champs-Libres Coop
|
||||
*/
|
||||
class LoadAccompanyingPeriod extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
|
||||
{
|
||||
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
|
||||
|
||||
public const ACCOMPANYING_PERIOD = 'parcours 1';
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 10004;
|
||||
}
|
||||
|
||||
public static $references = array();
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
|
||||
$centerA = $this->getReference('centerA');
|
||||
$centerAId = $centerA->getId();
|
||||
|
||||
$personIds = $this->container->get('doctrine.orm.entity_manager')
|
||||
->createQueryBuilder()
|
||||
->select('p.id')
|
||||
->from('ChillPersonBundle:Person', 'p')
|
||||
->where('p.center = :centerAId')
|
||||
->orderBy('p.id', 'ASC')
|
||||
->setParameter('centerAId', $centerAId)
|
||||
->getQuery()
|
||||
->getScalarResult();
|
||||
|
||||
$openingDate = new \DateTime('2020-04-01');
|
||||
|
||||
$person1 = $manager->getRepository(Person::class)->find($personIds[0]);
|
||||
$person2 = $manager->getRepository(Person::class)->find($personIds[1]);
|
||||
|
||||
$socialScope = $this->getReference('scope_social');
|
||||
|
||||
$a = new AccompanyingPeriod($openingDate);
|
||||
$a->addPerson($person1);
|
||||
$a->addPerson($person2);
|
||||
$a->addScope($socialScope);
|
||||
$a->setStep(AccompanyingPeriod::STEP_CONFIRMED);
|
||||
|
||||
$manager->persist($a);
|
||||
|
||||
$this->addReference(self::ACCOMPANYING_PERIOD, $a);
|
||||
echo "Adding one AccompanyingPeriod\n";
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
|
||||
|
||||
/**
|
||||
* Description of LoadAccompanyingPeriodOrigin
|
||||
*
|
||||
* @author Champs-Libres Coop
|
||||
*/
|
||||
class LoadAccompanyingPeriodOrigin extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
|
||||
public const ACCOMPANYING_PERIOD_ORIGIN = 'accompanying_period_origin';
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 10005;
|
||||
}
|
||||
|
||||
private $phoneCall = ['en' => 'phone call', 'fr' => 'appel téléphonique'];
|
||||
|
||||
public static $references = array();
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$o = new Origin();
|
||||
$o->setLabel(json_encode($this->phoneCall));
|
||||
|
||||
$manager->persist($o);
|
||||
|
||||
$this->addReference(self::ACCOMPANYING_PERIOD_ORIGIN, $o);
|
||||
echo "Adding one AccompanyingPeriod Origin\n";
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
@@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
|
||||
/**
|
||||
* Load people into database
|
||||
@@ -37,17 +38,17 @@ use Chill\MainBundle\Entity\Address;
|
||||
* @author Marc Ducobu <marc@champs-libres.coop>
|
||||
*/
|
||||
class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
|
||||
{
|
||||
|
||||
{
|
||||
|
||||
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
|
||||
|
||||
protected $faker;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->faker = \Faker\Factory::create('fr_FR');
|
||||
}
|
||||
|
||||
|
||||
public function prepare()
|
||||
{
|
||||
//prepare days, month, years
|
||||
@@ -56,57 +57,57 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
$this->years[] = $y;
|
||||
$y = $y +1;
|
||||
} while ($y >= 1990);
|
||||
|
||||
|
||||
$m = 1;
|
||||
do {
|
||||
$this->month[] = $m;
|
||||
$m = $m +1;
|
||||
} while ($m >= 12);
|
||||
|
||||
|
||||
$d = 1;
|
||||
do {
|
||||
$this->day[] = $d;
|
||||
$d = $d + 1;
|
||||
} while ($d <= 28);
|
||||
}
|
||||
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 10000;
|
||||
}
|
||||
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadRandPeople($manager);
|
||||
$this->loadExpectedPeople($manager);
|
||||
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
|
||||
public function loadExpectedPeople(ObjectManager $manager)
|
||||
{
|
||||
echo "loading expected people...\n";
|
||||
|
||||
|
||||
foreach ($this->peoples as $person) {
|
||||
$this->addAPerson($this->fillWithDefault($person), $manager);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function loadRandPeople(ObjectManager $manager)
|
||||
{
|
||||
echo "loading rand people...\n";
|
||||
|
||||
|
||||
$this->prepare();
|
||||
|
||||
|
||||
$chooseLastNameOrTri = array('tri', 'tri', 'name', 'tri');
|
||||
|
||||
|
||||
$i = 0;
|
||||
|
||||
|
||||
do {
|
||||
$i++;
|
||||
|
||||
|
||||
$sex = $this->genders[array_rand($this->genders)];
|
||||
|
||||
|
||||
if ($chooseLastNameOrTri[array_rand($chooseLastNameOrTri)] === 'tri' ) {
|
||||
$length = rand(2, 3);
|
||||
$lastName = '';
|
||||
@@ -117,13 +118,13 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
} else {
|
||||
$lastName = $this->lastNames[array_rand($this->lastNames)];
|
||||
}
|
||||
|
||||
|
||||
if ($sex === Person::MALE_GENDER) {
|
||||
$firstName = $this->firstNamesMale[array_rand($this->firstNamesMale)];
|
||||
} else {
|
||||
$firstName = $this->firstNamesFemale[array_rand($this->firstNamesFemale)];
|
||||
}
|
||||
|
||||
|
||||
// add an address on 80% of the created people
|
||||
if (rand(0,100) < 80) {
|
||||
$address = $this->getRandomAddress();
|
||||
@@ -137,7 +138,7 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
} else {
|
||||
$address = null;
|
||||
}
|
||||
|
||||
|
||||
$person = array(
|
||||
'FirstName' => $firstName,
|
||||
'LastName' => $lastName,
|
||||
@@ -147,15 +148,15 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
'Address' => $address,
|
||||
'maritalStatus' => $this->maritalStatusRef[array_rand($this->maritalStatusRef)]
|
||||
);
|
||||
|
||||
|
||||
$this->addAPerson($this->fillWithDefault($person), $manager);
|
||||
|
||||
|
||||
} while ($i <= 100);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* fill a person array with default value
|
||||
*
|
||||
*
|
||||
* @param string[] $specific
|
||||
*/
|
||||
private function fillWithDefault(array $specific)
|
||||
@@ -171,10 +172,10 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
'Address' => null
|
||||
), $specific);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* create a new person from array data
|
||||
*
|
||||
*
|
||||
* @param array $person
|
||||
* @param ObjectManager $manager
|
||||
* @throws \Exception
|
||||
@@ -200,35 +201,51 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
$this->addAccompanyingPeriods($p, $value, $manager);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
//try to add the data using the setSomething function,
|
||||
// if not possible, fallback to addSomething function
|
||||
if (method_exists($p, 'set'.$key)) {
|
||||
call_user_func(array($p, 'set'.$key), $value);
|
||||
} elseif (method_exists($p, 'add'.$key)) {
|
||||
// if we have a "addSomething", we may have multiple items to add
|
||||
// so, we set the value in an array if it is not an array, and
|
||||
// so, we set the value in an array if it is not an array, and
|
||||
// will call the function addSomething multiple times
|
||||
if (!is_array($value)) {
|
||||
$value = array($value);
|
||||
}
|
||||
|
||||
|
||||
foreach($value as $v) {
|
||||
if ($v !== NULL) {
|
||||
call_user_func(array($p, 'add'.$key), $v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$manager->persist($p);
|
||||
echo "add person'".$p->__toString()."'\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creata a random address
|
||||
*
|
||||
* Create a random point
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
private function getRandomPoint()
|
||||
{
|
||||
$lonBrussels = 4.35243;
|
||||
$latBrussels = 50.84676;
|
||||
$lon = $lonBrussels + 0.01 * rand(-5, 5);
|
||||
$lat = $latBrussels + 0.01 * rand(-5, 5);
|
||||
return Point::fromLonLat($lon, $lat);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a random address
|
||||
*
|
||||
* @return Address
|
||||
*/
|
||||
private function getRandomAddress()
|
||||
@@ -238,13 +255,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
->setStreetAddress2(
|
||||
rand(0,9) > 5 ? $this->faker->streetAddress : ''
|
||||
)
|
||||
->setPoint(
|
||||
rand(0,9) > 5 ? $this->getRandomPoint() : NULL
|
||||
)
|
||||
->setPostcode($this->getReference(
|
||||
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)]
|
||||
))
|
||||
->setValidFrom($this->faker->dateTimeBetween('-5 years'))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
private function getCountry($countryCode)
|
||||
{
|
||||
if ($countryCode === NULL) {
|
||||
@@ -257,30 +277,30 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
|
||||
private $maritalStatusRef = ['ms_single', 'ms_married', 'ms_widow', 'ms_separat',
|
||||
'ms_divorce', 'ms_legalco', 'ms_unknown'];
|
||||
|
||||
|
||||
private $firstNamesMale = array("Jean", "Mohamed", "Alfred", "Robert", "Justin", "Brian",
|
||||
"Compère", "Jean-de-Dieu", "Charles", "Pierre", "Luc", "Mathieu", "Alain", "Etienne", "Eric",
|
||||
"Corentin", "Gaston", "Spirou", "Fantasio", "Mahmadou", "Mohamidou", "Vursuv", "Youssef" );
|
||||
|
||||
|
||||
private $firstNamesFemale = array("Svedana", "Sevlatina", "Irène", "Marcelle",
|
||||
"Corentine", "Alfonsine", "Caroline", "Solange", "Gostine", "Fatoumata", "Nicole",
|
||||
"Groseille", "Chana", "Oxana", "Ivana", "Julie", "Tina", "Adèle" );
|
||||
|
||||
|
||||
private $lastNames = array("Diallo", "Bah", "Gaillot", "Martin");
|
||||
|
||||
|
||||
private $lastNamesTrigrams = array("fas", "tré", "hu", 'blart', 'van', 'der', 'lin', 'den',
|
||||
'ta', 'mi', 'net', 'gna', 'bol', 'sac', 'ré', 'jo', 'du', 'pont', 'cas', 'tor', 'rob', 'al',
|
||||
'ma', 'gone', 'car',"fu", "ka", "lot", "no", "va", "du", "bu", "su", "jau", "tte", 'sir',
|
||||
"lo", 'to', "cho", "car", 'mo','zu', 'qi', 'mu');
|
||||
|
||||
|
||||
private $genders = array(Person::MALE_GENDER, Person::FEMALE_GENDER);
|
||||
|
||||
|
||||
private $years = array();
|
||||
|
||||
|
||||
private $month = array();
|
||||
|
||||
|
||||
private $day = array();
|
||||
|
||||
|
||||
private $peoples = array(
|
||||
array(
|
||||
'LastName' => "Depardieu",
|
||||
@@ -362,21 +382,21 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
|
||||
'maritalStatus' => 'ms_legalco'
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
private function addAccompanyingPeriods(Person $person, array $periods, ObjectManager $manager)
|
||||
{
|
||||
foreach ($periods as $period) {
|
||||
|
||||
|
||||
echo "adding new past Accompanying Period..\n";
|
||||
|
||||
|
||||
/** @var AccompanyingPeriod $accompanyingPeriod */
|
||||
$accompanyingPeriod = new AccompanyingPeriod(new \DateTime($period['from']));
|
||||
$accompanyingPeriod
|
||||
->setClosingDate(new \DateTime($period['to']))
|
||||
->setRemark($period['remark'])
|
||||
;
|
||||
|
||||
|
||||
$person->addAccompanyingPeriod($accompanyingPeriod);
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
|
||||
/**
|
||||
* Add a role CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE for all groups except administrative,
|
||||
@@ -44,6 +45,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef);
|
||||
$scopeSocial = $this->getReference('scope_social');
|
||||
|
||||
//create permission group
|
||||
switch ($permissionsGroup->getName()) {
|
||||
@@ -51,6 +53,12 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
case 'direction':
|
||||
printf("Adding CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE to %s permission group \n", $permissionsGroup->getName());
|
||||
|
||||
$permissionsGroup->addRoleScope(
|
||||
(new RoleScope())
|
||||
->setRole(AccompanyingPeriodVoter::SEE)
|
||||
->setScope($scopeSocial)
|
||||
);
|
||||
|
||||
$roleScopeUpdate = (new RoleScope())
|
||||
->setRole('CHILL_PERSON_UPDATE')
|
||||
->setScope(null);
|
||||
|
@@ -28,6 +28,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class ChillPersonExtension
|
||||
@@ -76,6 +77,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
$loader->load('services/templating.yaml');
|
||||
$loader->load('services/alt_names.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
|
||||
// load service advanced search only if configure
|
||||
if ($config['search']['search_by_phone'] != 'never') {
|
||||
@@ -307,6 +309,55 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
'template' => '@ChillPerson/MaritalStatus/edit.html.twig',
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
'apis' => [
|
||||
[
|
||||
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod::class,
|
||||
'name' => 'accompanying_course',
|
||||
'base_path' => '/api/1.0/person/accompanying-course',
|
||||
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
|
||||
'actions' => [
|
||||
'_entity' => [
|
||||
'roles' => [
|
||||
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
|
||||
]
|
||||
],
|
||||
'participation' => [
|
||||
'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
|
||||
]
|
||||
]
|
||||
|
||||
]
|
||||
],
|
||||
[
|
||||
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class,
|
||||
'name' => 'accompanying_period_origin',
|
||||
'base_path' => '/api/1.0/person/accompanying-period/origin',
|
||||
'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
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
@@ -118,7 +118,7 @@ class AccompanyingPeriod
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
|
||||
* mappedBy="accompanyingPeriod",
|
||||
* cascade={"persist", "remove", "merge", "detach"})
|
||||
* cascade={"persist", "refresh", "remove", "merge", "detach"})
|
||||
*/
|
||||
private $participations;
|
||||
|
||||
@@ -344,50 +344,68 @@ class AccompanyingPeriod
|
||||
}
|
||||
|
||||
/**
|
||||
* This private function scan Participations Collection,
|
||||
* searching for a given Person
|
||||
* Get the participation containing a person
|
||||
*/
|
||||
private function participationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation
|
||||
public function getParticipationsContainsPerson(Person $person): Collection
|
||||
{
|
||||
foreach ($this->participations as $participation) {
|
||||
/** @var AccompanyingPeriodParticipation $participation */
|
||||
if ($person === $participation->getPerson()) {
|
||||
return $participation;
|
||||
}}
|
||||
|
||||
return null;
|
||||
return $this->getParticipations($person)->filter(
|
||||
function(AccompanyingPeriodParticipation $participation) use ($person) {
|
||||
if ($person === $participation->getPerson()) {
|
||||
return $participation;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This public function is the same but return only true or false
|
||||
* Get the opened participation containing a person
|
||||
*
|
||||
* "Open" means that the closed date is NULL
|
||||
*/
|
||||
public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation
|
||||
{
|
||||
$collection = $this->getParticipationsContainsPerson($person)->filter(
|
||||
function(AccompanyingPeriodParticipation $participation) use ($person) {
|
||||
if (NULL === $participation->getEndDate()) {
|
||||
return $participation;
|
||||
}
|
||||
});
|
||||
|
||||
return $collection->count() > 0 ? $collection->first() : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the accompanying period contains a person.
|
||||
*
|
||||
* **Note**: this participation can be opened or not.
|
||||
*/
|
||||
public function containsPerson(Person $person): bool
|
||||
{
|
||||
return ($this->participationsContainsPerson($person) === null) ? false : true;
|
||||
return $this->getParticipationsContainsPerson($person)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Person
|
||||
*/
|
||||
public function addPerson(Person $person = null): self
|
||||
public function addPerson(Person $person = null): AccompanyingPeriodParticipation
|
||||
{
|
||||
$participation = new AccompanyingPeriodParticipation($this, $person);
|
||||
$this->participations[] = $participation;
|
||||
|
||||
return $this;
|
||||
return $participation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Person
|
||||
*/
|
||||
public function removePerson(Person $person): void
|
||||
public function removePerson(Person $person): ?AccompanyingPeriodParticipation
|
||||
{
|
||||
$participation = $this->participationsContainsPerson($person);
|
||||
$participation = $this->getOpenParticipationContainsPerson($person);
|
||||
|
||||
if (! null === $participation) {
|
||||
if ($participation instanceof AccompanyingPeriodParticipation) {
|
||||
$participation->setEndDate(new \DateTimeImmutable('now'));
|
||||
$this->participations->removeElement($participation);
|
||||
}
|
||||
|
||||
return $participation;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -22,7 +22,7 @@
|
||||
|
||||
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ class Origin
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
public function getLabel()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
70
src/Bundle/ChillPersonBundle/Entity/Household/Household.php
Normal file
70
src/Bundle/ChillPersonBundle/Entity/Household/Household.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Entity\Household;
|
||||
|
||||
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=HouseholdRepository::class)
|
||||
*/
|
||||
class Household
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses
|
||||
* @var Collection
|
||||
*
|
||||
* @ORM\ManyToMany(
|
||||
* targetEntity="Chill\MainBundle\Entity\Address",
|
||||
* cascade={"persist", "remove", "merge", "detach"})
|
||||
* @ORM\JoinTable(name="chill_person_household_to_addresses")
|
||||
* @ORM\OrderBy({"validFrom" = "DESC"})
|
||||
*/
|
||||
private $addresses;
|
||||
|
||||
|
||||
/**
|
||||
* @param Address $address
|
||||
* @return $this
|
||||
*/
|
||||
public function addAddress(Address $address)
|
||||
{
|
||||
$this->addresses[] = $address;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Address $address
|
||||
*/
|
||||
public function removeAddress(Address $address)
|
||||
{
|
||||
$this->addresses->removeElement($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the addresses are ordered by date, descending (the most
|
||||
* recent first)
|
||||
*
|
||||
* @return \Chill\MainBundle\Entity\Address[]
|
||||
*/
|
||||
public function getAddresses()
|
||||
{
|
||||
return $this->addresses;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Entity\Household;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\PersonBundle\Repository\Household\HouseholdMembersRepository;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=HouseholdMembersRepository::class)
|
||||
*/
|
||||
class HouseholdMembers
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date")
|
||||
*/
|
||||
private $startDate;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date")
|
||||
*/
|
||||
private $endDate;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean")
|
||||
*/
|
||||
private $sharedHousehold;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Person
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\PersonBundle\Entity\Person"
|
||||
* )
|
||||
*/
|
||||
private $person;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Household
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\PersonBundle\Entity\Household\Household"
|
||||
* )
|
||||
*/
|
||||
private $household;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPosition(): ?string
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(?string $position): self
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
public function setStartDate(\DateTimeInterface $startDate): self
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
public function setEndDate(\DateTimeInterface $endDate): self
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComment(): ?string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function setComment(?string $comment): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSharedHousehold(): ?bool
|
||||
{
|
||||
return $this->sharedHousehold;
|
||||
}
|
||||
|
||||
public function setSharedHousehold(bool $sharedHousehold): self
|
||||
{
|
||||
$this->sharedHousehold = $sharedHousehold;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPerson(): ?Person
|
||||
{
|
||||
return $this->person;
|
||||
}
|
||||
|
||||
public function setPerson(?Person $person): self
|
||||
{
|
||||
$this->person = $person;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHousehold(): ?Household
|
||||
{
|
||||
return $this->household;
|
||||
}
|
||||
|
||||
public function setHousehold(?Household $household): self
|
||||
{
|
||||
$this->household = $household;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -390,7 +390,7 @@ class Person implements HasCenterInterface
|
||||
*
|
||||
* @deprecated since 1.1 use `getOpenedAccompanyingPeriod instead
|
||||
*/
|
||||
public function getCurrentAccompanyingPeriod() : AccompanyingPeriod
|
||||
public function getCurrentAccompanyingPeriod() : ?AccompanyingPeriod
|
||||
{
|
||||
return $this->getOpenedAccompanyingPeriod();
|
||||
}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository\Household;
|
||||
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMembers;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method HouseholdMembers|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method HouseholdMembers|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method HouseholdMembers[] findAll()
|
||||
* @method HouseholdMembers[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class HouseholdMembersRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, HouseholdMembers::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return HouseholdMembers[] Returns an array of HouseholdMembers objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('h')
|
||||
->andWhere('h.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('h.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?HouseholdMembers
|
||||
{
|
||||
return $this->createQueryBuilder('h')
|
||||
->andWhere('h.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Repository\Household;
|
||||
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method Household|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Household|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Household[] findAll()
|
||||
* @method Household[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class HouseholdRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Household::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Household[] Returns an array of Household objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('h')
|
||||
->andWhere('h.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('h.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?Household
|
||||
{
|
||||
return $this->createQueryBuilder('h')
|
||||
->andWhere('h.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<accompanying-course v-bind:accompanying_course="accompanying_course"/>
|
||||
<persons-associated v-bind:persons_associated="accompanying_course.persons"/>
|
||||
<requestor v-bind:accompanying_course="accompanying_course"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccompanyingCourse from './components/AccompanyingCourse.vue';
|
||||
import PersonsAssociated from './components/PersonsAssociated.vue';
|
||||
import Requestor from './components/Requestor.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AccompanyingCourse,
|
||||
PersonsAssociated,
|
||||
Requestor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accompanying_course: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
accompanyingCourseId() {
|
||||
return window.accompanyingCourseId;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getAccompanyingCourse() {
|
||||
let data_;
|
||||
return fetch(`/fr/api/parcours/${accompanyingCourseId}/show`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.$data.accompanying_course = data;
|
||||
});
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAccompanyingCourse();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>Parcours</h3>
|
||||
<dl>
|
||||
<dt>id</dt>
|
||||
<dd>{{ accompanying_course.id }}</dd>
|
||||
<dt>opening_date</dt>
|
||||
<dd>{{ accompanying_course.opening_date }}</dd>
|
||||
<dt>closing_date</dt>
|
||||
<dd>{{ accompanying_course.closing_date }}</dd>
|
||||
<dt>remark</dt>
|
||||
<dd>{{ accompanying_course.remark }}</dd>
|
||||
<dt>closing_motive</dt>
|
||||
<dd>{{ accompanying_course.closing_motive }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AccompanyingCourse',
|
||||
props: {
|
||||
accompanying_course: Object
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td>{{ person.firstname }}</td>
|
||||
<td>{{ person.lastname }}</td>
|
||||
<td>{{ person.startdate }}</td>
|
||||
<td>{{ person.enddate }}</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li><button class="sc-button bt-show"></button></li>
|
||||
<li><button class="sc-button bt-update"></button></li>
|
||||
<li><button class="sc-button bt-delete" @click.prevent="$emit('remove', person)"></button></li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PersonItem',
|
||||
props: {
|
||||
person: { type: Object, required: true }
|
||||
},
|
||||
emits: ['remove']
|
||||
}
|
||||
</script>
|
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>Usagers concernés</h3>
|
||||
|
||||
<label>{{ counter }} usagers</label>
|
||||
|
||||
<table class="rounded">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="chill-orange">firstname</th>
|
||||
<th class="chill-orange">lastname</th>
|
||||
<th class="chill-orange">startdate</th>
|
||||
<th class="chill-orange">enddate</th>
|
||||
<th class="chill-orange">actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<person-item
|
||||
v-for="person in persons_associated"
|
||||
v-bind:person="person"
|
||||
v-bind:key="person.id"
|
||||
@remove="removePerson" />
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li><button class="sc-button bt-create" @click="addPerson">Ajouter un usager</button></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PersonItem from "./PersonItem.vue"
|
||||
|
||||
export default {
|
||||
name: 'PersonsAssociated',
|
||||
components: {
|
||||
PersonItem
|
||||
},
|
||||
props: {
|
||||
persons_associated: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
persons: this.persons_associated
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
async counter() {
|
||||
// Pourquoi je peux pas compter un tableau avec length ???!!!
|
||||
return this.persons_associated.length // <= boum !
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addPerson() {
|
||||
this.persons_associated.push({
|
||||
"firstname": "Lisa",
|
||||
"lastname": "Simpson",
|
||||
"startdate": "1975-09-15",
|
||||
"enddate": "2021-04-20"
|
||||
})
|
||||
},
|
||||
removePerson(item) {
|
||||
this.persons_associated = this.persons_associated.filter(person => person !== item)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>Demandeur</h3>
|
||||
{{ accompanying_course.id }}
|
||||
{{ accompanying_course.remark }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Requestor',
|
||||
props: {
|
||||
accompanying_course: Object
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,8 +0,0 @@
|
||||
import App from './App.vue';
|
||||
import { createApp } from 'vue';
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`
|
||||
})
|
||||
.component('app', App)
|
||||
.mount('#accompanying-course');
|
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<accompanying-course></accompanying-course>
|
||||
<persons-associated></persons-associated>
|
||||
<requestor></requestor>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import AccompanyingCourse from './components/AccompanyingCourse.vue';
|
||||
import PersonsAssociated from './components/PersonsAssociated.vue';
|
||||
import Requestor from './components/Requestor.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AccompanyingCourse,
|
||||
PersonsAssociated,
|
||||
Requestor
|
||||
},
|
||||
computed: mapState([
|
||||
'accompanyingCourse'
|
||||
])
|
||||
};
|
||||
</script>
|
@@ -0,0 +1,49 @@
|
||||
const
|
||||
locale = 'fr',
|
||||
format = 'json'
|
||||
, accompanying_period_id = window.accompanyingCourseId //tmp
|
||||
;
|
||||
|
||||
/*
|
||||
* Endpoint chill_person_accompanying_course_api_show
|
||||
* method GET, get AccompanyingCourse Object
|
||||
*
|
||||
* @accompanying_period_id___ integer
|
||||
* @TODO var is not used but necessary in method signature
|
||||
*/
|
||||
let getAccompanyingCourse = (accompanying_period_id___) => { //tmp
|
||||
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/show.${format}`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw Error('Error with request resource response');
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Endpoint chill_person_accompanying_course_api_add_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
|
||||
*/
|
||||
let postParticipation = (accompanying_period_id, person_id, method) => {
|
||||
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/participation.${format}`
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({id: person_id})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw Error('Error with request resource response');
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
getAccompanyingCourse,
|
||||
postParticipation
|
||||
};
|
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>{{ $t('course.title') }}</h3>
|
||||
<dl>
|
||||
<dt>{{ $t('course.id') }}</dt>
|
||||
<dd>{{ accompanyingCourse.id }}</dd>
|
||||
<dt>{{ $t('course.opening_date') }}</dt>
|
||||
<dd>{{ $d(accompanyingCourse.openingDate.datetime, 'short') }}</dd>
|
||||
<dt>{{ $t('course.closing_date') }}</dt>
|
||||
<dd>{{ $d(accompanyingCourse.closingDate.datetime, 'short') }}</dd>
|
||||
<dt>{{ $t('course.remark') }}</dt>
|
||||
<dd>{{ accompanyingCourse.remark }}</dd>
|
||||
<dt>{{ $t('course.closing_motive') }}</dt>
|
||||
<dd>{{ accompanyingCourse.closing_motive }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AccompanyingCourse',
|
||||
computed: {
|
||||
accompanyingCourse() {
|
||||
return this.$store.state.accompanyingCourse
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td>{{ participation.person.firstName }}</td>
|
||||
<td>{{ participation.person.lastName }}</td>
|
||||
<td><span v-if="participation.startDate">
|
||||
{{ $d(participation.startDate.datetime, 'short') }}</span>
|
||||
</td>
|
||||
<td><span v-if="participation.endDate">
|
||||
{{ $d(participation.endDate.datetime, 'short') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="sc-button bt-show" target="_blank"
|
||||
:href="url.show"
|
||||
:title="$t('action.show')">
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="sc-button bt-update" target="_blank"
|
||||
:href="url.edit"
|
||||
:title="$t('action.edit')">
|
||||
</a>
|
||||
</li>
|
||||
<!--li>
|
||||
<button class="sc-button bt-delete"
|
||||
:title="$t('action.delete')"
|
||||
@click.prevent="$emit('remove', participation)">
|
||||
</button>
|
||||
</li-->
|
||||
<li>
|
||||
<button class="sc-button bt-remove"
|
||||
:title="$t('action.remove')"
|
||||
@click.prevent="$emit('close', participation)">
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PersonItem',
|
||||
props: ['participation'],
|
||||
data() {
|
||||
return {
|
||||
url: {
|
||||
show: '/fr/person/' + this.participation.person.id + '/general',
|
||||
edit: '/fr/person/' + this.participation.person.id + '/general/edit'
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'close']
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>{{ $t('persons_associated.title')}}</h3>
|
||||
<label>{{ $tc('persons_associated.counter', counter) }}</label>
|
||||
<table class="rounded">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th>
|
||||
<th class="chill-orange">{{ $t('persons_associated.lastname') }}</th>
|
||||
<th class="chill-orange">{{ $t('persons_associated.startdate') }}</th>
|
||||
<th class="chill-orange">{{ $t('persons_associated.enddate') }}</th>
|
||||
<th class="chill-orange">{{ $t('action.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<person-item
|
||||
v-for="participation in participations"
|
||||
v-bind:participation="participation"
|
||||
v-bind:key="participation.id"
|
||||
@remove="removeParticipation"
|
||||
@close="closeParticipation">
|
||||
</person-item>
|
||||
</tbody>
|
||||
</table>
|
||||
<add-persons></add-persons>
|
||||
<ul class="record_actions">
|
||||
<!--li>
|
||||
<button class="sc-button orange" @click="savePersons">
|
||||
{{ $t('action.save') }}
|
||||
</button>
|
||||
</li-->
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import PersonItem from "./PersonItem.vue"
|
||||
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
|
||||
|
||||
export default {
|
||||
name: 'PersonsAssociated',
|
||||
components: {
|
||||
PersonItem,
|
||||
AddPersons
|
||||
},
|
||||
computed: mapState({
|
||||
participations: state => state.accompanyingCourse.participations,
|
||||
counter: state => state.accompanyingCourse.participations.length
|
||||
}),
|
||||
methods: {
|
||||
removeParticipation(item) {
|
||||
this.$store.dispatch('removeParticipation', item)
|
||||
},
|
||||
closeParticipation(item) {
|
||||
console.log('@@ CLICK close participation: item', item);
|
||||
this.$store.dispatch('closeParticipation', item)
|
||||
},
|
||||
/*
|
||||
savePersons() {
|
||||
console.log('[wip] saving persons');
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="vue-component">
|
||||
<h3>{{ $t('requestor.title') }}</h3>
|
||||
{{ accompanyingCourse.id }}
|
||||
{{ accompanyingCourse.remark }}<br><br>
|
||||
|
||||
<!-- TESTS AREA -->
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button class="sc-button bt-create" @click="modal1.showModal = true">
|
||||
{{ $t('action.show_modal') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="sc-button bt-create" @click="modal2.showModal = true">
|
||||
Ouvrir une seconde modale
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<teleport to="body">
|
||||
<modal v-if="modal1.showModal" :modalDialogClass="modal1.modalDialogClass" @close="modal1.showModal = false">
|
||||
<template v-slot:header>
|
||||
<h3 class="modal-title">Le titre de ma modale</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
|
||||
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
|
||||
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
|
||||
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="sc-button green" @click="modal1.showModal = false; modal2.showModal = true">
|
||||
{{ $t('action.next')}}</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
|
||||
<teleport to="body">
|
||||
<modal v-if="modal2.showModal" :modalDialogClass="modal2.modalDialogClass" @close="modal2.showModal = false">
|
||||
<template v-slot:header>
|
||||
<h3 class="modal-title">Une autre modale</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>modal 2</p>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="sc-button green" @click="modal2.showModal = false">
|
||||
{{ $t('action.save')}}</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<!-- END TESTS -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
|
||||
|
||||
export default {
|
||||
name: 'Requestor',
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal1: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl" // modal-lg modal-md modal-sm
|
||||
},
|
||||
modal2: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-centered modal-sm" // modal-lg modal-md modal-sm
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
accompanyingCourse() {
|
||||
return this.$store.state.accompanyingCourse
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
|
||||
import { appMessages } from './js/i18n'
|
||||
import { initPromise } from './store'
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
initPromise.then(store => {
|
||||
|
||||
//console.log('store in create_store', store);
|
||||
//console.log('store accompanyingCourse', store.state.accompanyingCourse);
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.component('app', App)
|
||||
.mount('#accompanying-course');
|
||||
|
||||
});
|
@@ -0,0 +1,32 @@
|
||||
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
course: {
|
||||
id: "id",
|
||||
title: "Parcours",
|
||||
opening_date: "Date d'ouverture",
|
||||
closing_date: "Date de clôture",
|
||||
remark: "Commentaire",
|
||||
closing_motive: "Motif de clôture",
|
||||
},
|
||||
persons_associated: {
|
||||
title: "Usagers concernés",
|
||||
counter: "Pas d'usager | 1 usager | {count} usagers",
|
||||
firstname: "Prénom",
|
||||
lastname: "Nom",
|
||||
startdate: "Date d'entrée",
|
||||
enddate: "Date de sortie",
|
||||
addPerson: "Ajouter un usager",
|
||||
},
|
||||
requestor: {
|
||||
title: "Demandeur",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(appMessages.fr, personMessages.fr);
|
||||
|
||||
export {
|
||||
appMessages
|
||||
};
|
@@ -0,0 +1,76 @@
|
||||
import 'es6-promise/auto';
|
||||
import { createStore } from 'vuex';
|
||||
import addPersons from './modules/addPersons'
|
||||
import { getAccompanyingCourse, postParticipation } from '../api';
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const id = window.accompanyingCourseId; //tmp
|
||||
|
||||
let initPromise = getAccompanyingCourse(id)
|
||||
.then(accompanying_course => new Promise((resolve, reject) => {
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
modules: {
|
||||
addPersons
|
||||
},
|
||||
state: {
|
||||
accompanyingCourse: accompanying_course,
|
||||
errorMsg: []
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
removeParticipation(state, item) {
|
||||
//console.log('mutation: remove item', item.id);
|
||||
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== item);
|
||||
},
|
||||
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);
|
||||
},
|
||||
addParticipation(state, participation) {
|
||||
//console.log('### mutation: add participation', participation);
|
||||
state.accompanyingCourse.participations.push(participation);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
removeParticipation({ commit }, payload) {
|
||||
commit('removeParticipation', payload);
|
||||
},
|
||||
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);
|
||||
});
|
||||
},
|
||||
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);
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
//console.log('store object', store.state.accompanyingCourse.id);
|
||||
resolve(store);
|
||||
}));
|
||||
|
||||
export { initPromise };
|
@@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
const
|
||||
locale = 'fr',
|
||||
format = 'json'
|
||||
;
|
||||
|
||||
/*
|
||||
* 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}`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw Error('Error with request resource response');
|
||||
});
|
||||
};
|
||||
|
||||
export { searchPersons };
|
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<button class="sc-button bt-create centered mt-4" @click="openModal">
|
||||
{{ $t('add_persons.search_add_others_persons') }}
|
||||
</button>
|
||||
|
||||
<teleport to="body">
|
||||
<modal v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<h3 class="modal-title">{{ $t('add_persons.title') }}</h3>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-fixed>
|
||||
<div class="search">
|
||||
<label style="float: right;">
|
||||
{{ $tc('add_persons.suggested_counter', suggestedCounter) }}
|
||||
</label>
|
||||
|
||||
<input id="search-persons"
|
||||
name="query"
|
||||
v-model="query"
|
||||
:placeholder="$t('add_persons.search_some_persons')"
|
||||
ref="search" />
|
||||
<i class="fa fa-search fa-lg"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<!--span class="discret">Selection: {{ selected }}</span-->
|
||||
<div class="results">
|
||||
<div class="count">
|
||||
<span>
|
||||
<a v-if="suggestedCounter > 0" href="#">
|
||||
{{ $t('action.check_all')}}</a>
|
||||
<a v-if="selectedCounter > 0" href="#">
|
||||
{{ $t('action.reset')}}</a>
|
||||
</span>
|
||||
<span v-if="selectedCounter > 0">
|
||||
{{ $tc('add_persons.selected_counter', selectedCounter) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<person-suggestion
|
||||
v-for="item in this.selectedAndSuggested.slice().reverse()"
|
||||
v-bind:item="item"
|
||||
v-bind:key="item.id">
|
||||
</person-suggestion>
|
||||
|
||||
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
|
||||
{{ $t('action.create') }} "{{ query }}"
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<button class="sc-button green" @click="addNewPersons">
|
||||
<i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
|
||||
import PersonSuggestion from 'ChillPersonAssets/vuejs/_components/PersonSuggestion';
|
||||
|
||||
export default {
|
||||
name: 'AddPersons',
|
||||
components: {
|
||||
Modal,
|
||||
PersonSuggestion,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl"
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
addPersons: state => state.addPersons
|
||||
}),
|
||||
query: {
|
||||
set(query) {
|
||||
this.$store.dispatch('setQuery', { query });
|
||||
},
|
||||
get() {
|
||||
return this.addPersons.query;
|
||||
}
|
||||
},
|
||||
suggested() {
|
||||
return this.addPersons.suggested;
|
||||
},
|
||||
suggestedCounter() {
|
||||
return this.addPersons.suggested.length;
|
||||
},
|
||||
selected() {
|
||||
return this.addPersons.selected;
|
||||
},
|
||||
selectedCounter() {
|
||||
return this.addPersons.selected.length;
|
||||
},
|
||||
selectedAndSuggested() {
|
||||
return this.$store.getters.selectedAndSuggested;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
this.$nextTick(function() {
|
||||
this.$refs.search.focus();
|
||||
})
|
||||
},
|
||||
addNewPersons() {
|
||||
console.log('@@@ CLICK button addPersons')
|
||||
this.selected.forEach(function(item) {
|
||||
//console.log('# dispatch action for each item', item);
|
||||
this.$store.dispatch('addParticipation', item);
|
||||
}, this
|
||||
);
|
||||
this.modal.showModal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="list-item" :class="{ checked: isChecked }">
|
||||
<div class="container">
|
||||
|
||||
<!--a class="discret" target="_blank" :href="url.show">{{ item.id }}</a-->
|
||||
<input class=""
|
||||
type="checkbox"
|
||||
v-model="selected"
|
||||
:value="item" />
|
||||
|
||||
{{ item.text }}
|
||||
|
||||
</div>
|
||||
<div class="right_actions">
|
||||
|
||||
<span class="badge badge-pill badge-secondary" :title="item.id">
|
||||
{{ $t('item.type_person') }}
|
||||
</span>
|
||||
<a class="sc-button bt-show" target="_blank" :title="item.id" :href="url.show"></a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'PersonSuggestion',
|
||||
props: ['item'],
|
||||
data() {
|
||||
return {
|
||||
url: {
|
||||
show: '/fr/person/' + this.item.id + '/general',
|
||||
edit: '/fr/person/' + this.item.id + '/general/edit'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
set(value) {
|
||||
this.$store.dispatch('updateSelected', value);
|
||||
},
|
||||
get() {
|
||||
return this.$store.state.addPersons.selected;
|
||||
}
|
||||
},
|
||||
isChecked() {
|
||||
return (this.selected.indexOf(this.item) === -1) ? false : true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -0,0 +1,21 @@
|
||||
const personMessages = {
|
||||
fr: {
|
||||
add_persons: {
|
||||
search_add_others_persons: "Rechercher et ajouter d'autres usagers",
|
||||
title: "Ajouter des usagers",
|
||||
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats",
|
||||
selected_counter: " 1 sélectionné | {count} sélectionnés",
|
||||
search_some_persons: "Rechercher des personnes..",
|
||||
},
|
||||
item: {
|
||||
type_person: "Usager",
|
||||
type_tms: "TMS",
|
||||
type_3rdparty: "Tiers",
|
||||
type_menage: "Ménage"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
personMessages
|
||||
};
|
@@ -5,15 +5,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<div id="accompanying-course"></div>
|
||||
{% endblock %}
|
||||
|
||||
{{ encore_entry_script_tags('accompanying_course') }}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript">
|
||||
window.accompanyingCourseId = {{ accompanyingCourse.id|e('js') }};
|
||||
</script>
|
||||
|
||||
{{ encore_entry_script_tags('accompanying_course') }}
|
||||
{% endblock %}
|
||||
|
@@ -21,17 +21,17 @@
|
||||
{% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
|
||||
<h1>{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
|
||||
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
|
||||
{{ form_row(form.isNoAddress) }}
|
||||
{{ form_row(form.streetAddress1) }}
|
||||
{{ form_row(form.streetAddress2) }}
|
||||
{{ form_row(form.street) }}
|
||||
{{ form_row(form.streetNumber) }}
|
||||
{{ form_row(form.postCode) }}
|
||||
{{ form_row(form.validFrom) }}
|
||||
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
|
||||
@@ -42,7 +42,7 @@
|
||||
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-save' }, 'label': 'Save' } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
@@ -23,9 +23,9 @@
|
||||
{% block title %}{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
|
||||
<h1>{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
|
||||
|
||||
|
||||
<table class="records_list">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -48,11 +48,11 @@
|
||||
{% for address in person.addresses %}
|
||||
<tr>
|
||||
<td><strong>{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }}</strong></td>
|
||||
|
||||
|
||||
<td>
|
||||
{{ address_macros._render(address, { 'with_valid_from' : false, 'has_no_address': true } ) }}
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
@@ -61,11 +61,17 @@
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
|
||||
@@ -73,10 +79,11 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}" class="sc-button bt-create">
|
||||
{{ 'Add an address'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
@@ -21,21 +21,21 @@
|
||||
{% block title %}{{ 'New address for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
|
||||
<h1>{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
|
||||
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
|
||||
{{ form_row(form.isNoAddress) }}
|
||||
{{ form_row(form.streetAddress1) }}
|
||||
{{ form_errors(form.streetAddress1) }}
|
||||
{{ form_row(form.streetAddress2) }}
|
||||
{{ form_errors(form.streetAddress2) }}
|
||||
{{ form_row(form.street) }}
|
||||
{{ form_errors(form.street) }}
|
||||
{{ form_row(form.streetNumber) }}
|
||||
{{ form_errors(form.streetNumber) }}
|
||||
{{ form_row(form.postCode) }}
|
||||
{{ form_errors(form.postCode) }}
|
||||
{{ form_row(form.validFrom) }}
|
||||
{{ form_errors(form.validFrom) }}
|
||||
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
|
||||
@@ -46,7 +46,7 @@
|
||||
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
||||
{% endblock personcontent %}
|
||||
|
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
|
||||
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
protected AuthorizationHelper $helper;
|
||||
|
||||
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
|
||||
|
||||
/**
|
||||
* @param AuthorizationHelper $helper
|
||||
*/
|
||||
public function __construct(AuthorizationHelper $helper)
|
||||
{
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
return $subject instanceof AccompanyingPeriod;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO take scopes into account
|
||||
foreach ($subject->getPersons() as $person) {
|
||||
// give access as soon as on center is reachable
|
||||
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getAttributes()
|
||||
{
|
||||
return [
|
||||
self::SEE
|
||||
];
|
||||
}
|
||||
|
||||
public function getRoles()
|
||||
{
|
||||
return $this->getAttributes();
|
||||
}
|
||||
|
||||
public function getRolesWithoutScope()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRolesWithHierarchy()
|
||||
{
|
||||
return [ 'Person' => $this->getRoles() ];
|
||||
}
|
||||
|
||||
}
|
@@ -55,7 +55,7 @@ class PersonNormalizer implements
|
||||
'id' => $person->getId(),
|
||||
'firstName' => $person->getFirstName(),
|
||||
'lastName' => $person->getLastName(),
|
||||
'birthdate' => $person->getBirthdate() ? $this->normalizer->normalize($person->getBirthdate()) : null,
|
||||
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
|
||||
'center' => $this->normalizer->normalize($person->getCenter())
|
||||
];
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
/**
|
||||
* Test api for AccompanyingCourseControllerTest
|
||||
*/
|
||||
class AccompanyingCourseControllerTest extends WebTestCase
|
||||
class AccompanyingCourseApiControllerTest extends WebTestCase
|
||||
{
|
||||
protected static EntityManagerInterface $em;
|
||||
|
||||
@@ -65,7 +65,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
*/
|
||||
public function testAccompanyingCourseShow(int $personId, AccompanyingPeriod $period)
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId()));
|
||||
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
|
||||
@@ -77,6 +77,14 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
$this->assertGreaterThan(0, $data->participations);
|
||||
}
|
||||
|
||||
public function testShow404()
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', 99999));
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @dataProvider dataGenerateRandomAccompanyingCourse
|
||||
@@ -85,26 +93,55 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
sprintf('/fr/person/api/1.0/accompanying-course/%d/participation.json', $period->getId()),
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
|
||||
[], // parameters
|
||||
[], // files
|
||||
[], // server parameters
|
||||
\json_encode([ 'id' => $personId ])
|
||||
);
|
||||
$response = $this->client->getResponse();
|
||||
$data = \json_decode($response->getContent(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId()));
|
||||
$this->assertArrayHasKey('id', $data);
|
||||
$this->assertArrayHasKey('startDate', $data);
|
||||
$this->assertNotNull($data['startDate']);
|
||||
|
||||
// check by deownloading the accompanying cours
|
||||
|
||||
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$data = \json_decode($response->getContent());
|
||||
|
||||
// check that the person id is contained
|
||||
$participationsPersonsIds = \array_map(
|
||||
function($participation) { return $participation->person->id; },
|
||||
$data->participations);
|
||||
|
||||
$this->assertContains($personId, $participationsPersonsIds);
|
||||
|
||||
// check removing the participation
|
||||
$this->client->request(
|
||||
Request::METHOD_DELETE,
|
||||
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
|
||||
[], // parameters
|
||||
[], // files
|
||||
[], // server parameters
|
||||
\json_encode([ 'id' => $personId ])
|
||||
);
|
||||
$response = $this->client->getResponse();
|
||||
$data = \json_decode($response->getContent(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
|
||||
$this->assertArrayHasKey('id', $data);
|
||||
$this->assertArrayHasKey('startDate', $data);
|
||||
$this->assertNotNull($data['startDate']);
|
||||
$this->assertArrayHasKey('endDate', $data);
|
||||
$this->assertNotNull($data['endDate']);
|
||||
|
||||
|
||||
// set to variable for tear down
|
||||
$this->personId = $personId;
|
||||
$this->period = $period;
|
||||
}
|
||||
@@ -112,6 +149,7 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
protected function tearDown()
|
||||
{
|
||||
// remove participation created during test 'testAccompanyingCourseAddParticipation'
|
||||
// and if the test could not remove it
|
||||
|
||||
$testAddParticipationName = 'testAccompanyingCourseAddParticipation';
|
||||
|
||||
@@ -126,8 +164,10 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
|
||||
;
|
||||
|
||||
$em->remove($participation);
|
||||
$em->flush();
|
||||
if (NULL !== $participation) {
|
||||
$em->remove($participation);
|
||||
$em->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function dataGenerateRandomAccompanyingCourse()
|
||||
@@ -139,9 +179,9 @@ class AccompanyingCourseControllerTest extends WebTestCase
|
||||
// * one for getting the person, which will in turn provide his accompanying period;
|
||||
// * one for getting the personId to populate to the data manager
|
||||
//
|
||||
// Ensure to keep always $maxGenerated to the double of $maxResults
|
||||
$maxGenerated = 1;
|
||||
$maxResults = 15 * 8;
|
||||
// Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :)
|
||||
$maxGenerated = 3;
|
||||
$maxResults = $maxGenerated * 8;
|
||||
|
||||
static::bootKernel();
|
||||
$em = static::$container->get(EntityManagerInterface::class);
|
@@ -27,7 +27,6 @@ use Chill\PersonBundle\Entity\Person;
|
||||
|
||||
class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testClosingIsAfterOpeningConsistency()
|
||||
{
|
||||
$datetime1 = new \DateTime('now');
|
||||
@@ -77,22 +76,32 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertFalse($period->isOpen());
|
||||
}
|
||||
|
||||
public function testCanBeReOpened()
|
||||
public function testPersonPeriod()
|
||||
{
|
||||
$person = new Person(\DateTime::createFromFormat('Y-m-d', '2010-01-01'));
|
||||
$person->close($person->getAccompanyingPeriods()[0]
|
||||
->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2010-12-31')));
|
||||
|
||||
$firstAccompanygingPeriod = $person->getAccompanyingPeriodsOrdered()[0];
|
||||
|
||||
$this->assertTrue($firstAccompanygingPeriod->canBeReOpened());
|
||||
|
||||
$lastAccompanyingPeriod = (new AccompanyingPeriod(\DateTime::createFromFormat('Y-m-d', '2011-01-01')))
|
||||
->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2011-12-31'))
|
||||
;
|
||||
$person->addAccompanyingPeriod($lastAccompanyingPeriod);
|
||||
|
||||
$this->assertFalse($firstAccompanygingPeriod->canBeReOpened());
|
||||
}
|
||||
$person = new Person();
|
||||
$person2 = new Person();
|
||||
$person3 = new Person();
|
||||
$period = new AccompanyingPeriod(new \DateTime());
|
||||
|
||||
$period->addPerson($person);
|
||||
$period->addPerson($person2);
|
||||
$period->addPerson($person3);
|
||||
|
||||
$this->assertEquals(3, $period->getParticipations()->count());
|
||||
$this->assertTrue($period->containsPerson($person));
|
||||
$this->assertFalse($period->containsPerson(new Person()));
|
||||
|
||||
$participation = $period->getOpenParticipationContainsPerson($person);
|
||||
$participations = $period->getParticipationsContainsPerson($person);
|
||||
$this->assertNotNull($participation);
|
||||
$this->assertSame($person, $participation->getPerson());
|
||||
$this->assertEquals(1, $participations->count());
|
||||
|
||||
$participationL = $period->removePerson($person);
|
||||
$this->assertSame($participationL, $participation);
|
||||
$this->assertTrue($participation->getEndDate() instanceof \DateTimeInterface);
|
||||
|
||||
$participation = $period->getOpenParticipationContainsPerson($person);
|
||||
$this->assertNull($participation);
|
||||
}
|
||||
}
|
||||
|
@@ -78,7 +78,7 @@ abstract class AbstractTimelineAccompanyingPeriod implements TimelineProviderInt
|
||||
}
|
||||
|
||||
$metadata = $this->em
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod')
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriodParticipation')
|
||||
;
|
||||
|
||||
return array(
|
||||
|
@@ -46,8 +46,8 @@ class TimelineAccompanyingPeriodClosing extends AbstractTimelineAccompanyingPeri
|
||||
public function fetchQuery($context, array $args)
|
||||
{
|
||||
$metadata = $this->em
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod');
|
||||
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriodParticipation');
|
||||
|
||||
$data = $this->basicFetchQuery($context, $args);
|
||||
|
||||
$data['type'] = 'accompanying_period_closing';
|
||||
|
@@ -46,8 +46,8 @@ class TimelineAccompanyingPeriodOpening extends AbstractTimelineAccompanyingPeri
|
||||
public function fetchQuery($context, array $args)
|
||||
{
|
||||
$metadata = $this->em
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriod');
|
||||
|
||||
->getClassMetadata('ChillPersonBundle:AccompanyingPeriodParticipation');
|
||||
|
||||
$data = $this->basicFetchQuery($context, $args);
|
||||
|
||||
$data['type'] = 'accompanying_period_opening';
|
||||
|
@@ -8,5 +8,5 @@ module.exports = function(encore, entries)
|
||||
ChillPersonAssets: __dirname + '/Resources/public'
|
||||
});
|
||||
|
||||
encore.addEntry('accompanying_course', __dirname + '/Resources/public/js/AccompanyingCourse/index.js');
|
||||
encore.addEntry('accompanying_course', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
# cl_chill_person.example.class: Chill\PersonBundle\Example
|
||||
|
||||
services:
|
||||
services:
|
||||
|
||||
|
||||
chill.person.form.type.select2maritalstatus:
|
||||
@@ -27,14 +27,6 @@ services:
|
||||
public: true
|
||||
tags:
|
||||
- { name: chill.timeline, context: 'person' }
|
||||
|
||||
chill.person.security.authorization.person:
|
||||
class: Chill\PersonBundle\Security\Authorization\PersonVoter
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
chill.person.birthdate_validation:
|
||||
class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator
|
||||
|
@@ -46,3 +46,9 @@ services:
|
||||
$dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
|
||||
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Chill\PersonBundle\Controller\AccompanyingCourseApiController:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
|
||||
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
|
||||
tags: ['controller.service_arguments']
|
||||
|
@@ -23,3 +23,8 @@ services:
|
||||
arguments:
|
||||
- '@Doctrine\Persistence\ManagerRegistry'
|
||||
tags: [ doctrine.repository_service ]
|
||||
|
||||
Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository:
|
||||
arguments:
|
||||
- '@Doctrine\Persistence\ManagerRegistry'
|
||||
tags: [ doctrine.repository_service ]
|
||||
|
16
src/Bundle/ChillPersonBundle/config/services/security.yaml
Normal file
16
src/Bundle/ChillPersonBundle/config/services/security.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
chill.person.security.authorization.person:
|
||||
class: Chill\PersonBundle\Security\Authorization\PersonVoter
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create Household and HouseholdMembers tables
|
||||
*/
|
||||
final class Version20210505093408 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create Household and HouseholdMembers tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE Household_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE SEQUENCE HouseholdMembers_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE Household (id INT NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE TABLE HouseholdMembers (id INT NOT NULL, person_id INT DEFAULT NULL, household_id INT DEFAULT NULL, position VARCHAR(255) DEFAULT NULL, startDate DATE NOT NULL, endDate DATE NOT NULL, comment VARCHAR(255) DEFAULT NULL, sharedHousehold BOOLEAN NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_4D1FB288217BBB47 ON HouseholdMembers (person_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4D1FB288E79FF843 ON HouseholdMembers (household_id)');
|
||||
$this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288E79FF843');
|
||||
$this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288217BBB47');
|
||||
$this->addSql('DROP SEQUENCE Household_id_seq CASCADE');
|
||||
$this->addSql('DROP SEQUENCE HouseholdMembers_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE Household');
|
||||
$this->addSql('DROP TABLE HouseholdMembers');
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add a household_to_address table
|
||||
*/
|
||||
final class Version20210505154316 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a household_to_addresses table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE chill_person_household_to_addresses (household_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY(household_id, address_id))');
|
||||
$this->addSql('CREATE INDEX IDX_7109483E79FF843 ON chill_person_household_to_addresses (household_id)');
|
||||
$this->addSql('CREATE INDEX IDX_7109483F5B7AF75 ON chill_person_household_to_addresses (address_id)');
|
||||
$this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483F5B7AF75 FOREIGN KEY (address_id) REFERENCES chill_main_address (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE chill_person_household_to_addresses');
|
||||
}
|
||||
}
|
@@ -190,6 +190,7 @@ CHILL_PERSON_CREATE: Ajouter des personnes
|
||||
CHILL_PERSON_STATS: Statistiques sur les personnes
|
||||
CHILL_PERSON_LISTS: Liste des personnes
|
||||
CHILL_PERSON_DUPLICATE: Gérer les doublons de personnes
|
||||
CHILL_PERSON_ACCOMPANYING_PERIOD_SEE: Voir les périodes d'accompagnement
|
||||
|
||||
#period
|
||||
Period closed!: Période clôturée!
|
||||
|
Reference in New Issue
Block a user