Compare commits

..

17 Commits

Author SHA1 Message Date
Pol Dellaiera
1f9b4ddd79 Remove custom definition (form.yaml). 2021-05-06 21:37:54 +02:00
Pol Dellaiera
a9bdb1fe3b Move AccompanyingPeriodType, remove custom definition. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
92b6f4e206 Remove deprecation (Event). 2021-05-06 21:37:54 +02:00
Pol Dellaiera
c019da9bcf Remove templating.yaml, add interfaces. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
5146419e9a Remove privacyEvent.yaml + autowire logger. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
11036e84f6 Use interface instead of concrete implementation. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
1f488109e3 Fix TimelineBuilder service. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
d2a93f0763 Fix dependency injection. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
c5940c1263 Use EntityManagerInterface. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
7f602ffa47 Remove custom service definition, use auto-discovery feature. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
ac9d0242ad Fix controllers and use auto-discovery. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
e9a7a05fb7 Fix routes and use annotations. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
174d873398 refactor: Upgrade repositories. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
55695b7d2c Remove fixture test app, it's making conflicts.
TODO: See if this was still relevant and restored later
if needed.
2021-05-06 21:37:54 +02:00
Pol Dellaiera
ca3cb787d7 Fix migration classes namespaces. 2021-05-06 21:37:54 +02:00
Pol Dellaiera
84bb56664a Update ChillPersonBundle - Move files in src/ 2021-05-06 21:37:54 +02:00
Pol Dellaiera
f26eb06276 Fix webpack encore. 2021-05-06 17:27:07 +02:00
553 changed files with 2396 additions and 39798 deletions

3
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.composer/*
composer.phar
composer.lock
docs/build/
.php_cs.cache
###> symfony/framework-bundle ###
/.env.local
@@ -19,4 +19,3 @@ docs/build/
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

View File

@@ -18,7 +18,7 @@ before_script:
# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# See http://docs.gitlab.com/ee/ci/services/README.html for examples.
services:
- name: postgis/postgis:12-3.1-alpine
- name: postgres:12
alias: db
- name: redis
alias: redis

View File

@@ -13,7 +13,7 @@
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
"Chill\\FamilyMemberBundle\\": "src/Bundle/ChillFamilyMemberBundle",
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle/src",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle"
@@ -73,8 +73,7 @@
"symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8"
"symfony/phpunit-bridge": "^5.2"
},
"scripts": {
"auto-scripts": {

View File

@@ -1,747 +0,0 @@
.. 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.
Basic configuration
*******************
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);
}
}
Managing association
********************
ManyToOne association
=====================
In ManyToOne association, you can add associated entities using the :code:`PATCH` request. By default, the serializer deserialize entities only with their id and discriminator type, if any.
Example:
.. code-block:: bash
curl -X 'PATCH' \
'http://localhost:8001/api/1.0/person/accompanying-course/2668.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
# see the data sent to the server: \
-d '{
"type": "accompanying_period",
"id": 2668,
"origin": { "id": 11 }
}'
ManyToMany associations
=======================
In OneToMany association, you can easily create route for adding and removing entities, using :code:`POST` and :code:`DELETE` requests.
Prepare your entity, creating the methods :code:`addYourEntity` and :code:`removeYourEntity`:
.. code-block:: php
namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Entity\Scope;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* AccompanyingPeriod Class
*
* @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period")
* @DiscriminatorMap(typeProperty="type", mapping={
* "accompanying_period"=AccompanyingPeriod::class
* })
*/
class AccompanyingPeriod
{
/**
* @var Collection
* @ORM\ManyToMany(
* targetEntity=Scope::class,
* cascade={}
* )
* @Groups({"read"})
*/
private $scopes;
public function addScope(Scope $scope): self
{
$this->scopes[] = $scope;
return $this;
}
public function removeScope(Scope $scope): void
{
$this->scopes->removeElement($scope);
}
Create your route into the configuration:
.. code-block:: yaml
chill_main:
apis:
-
class: Chill\PersonBundle\Entity\AccompanyingPeriod
name: accompanying_course
base_path: /api/1.0/person/accompanying-course
controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
actions:
scope:
methods:
POST: true
DELETE: true
GET: false
HEAD: false
PUT: false
PATCH: false
roles:
POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
GET: null
HEAD: null
PUT: null
PATCH: null
controller_action: null
path: null
single-collection: single
This will create a new route, which will accept two methods: DELETE and POST:
.. code-block:: raw
+--------------+---------------------------------------------------------------------------------------+
| Property | Value |
+--------------+---------------------------------------------------------------------------------------+
| Route Name | chill_api_single_accompanying_course_scope |
| Path | /api/1.0/person/accompanying-course/{id}/scope.{_format} |
| Path Regex | {^/api/1\.0/person/accompanying\-course/(?P<id>[^/]++)/scope\.(?P<_format>[^/]++)$}sD |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | POST|DELETE |
| Requirements | {id}: \d+ |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: csapi_accompanying_course_controller:scopeApi |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+---------------------------------------------------------------------------------------+
Then, create the controller action. Call the method:
.. code-block:: php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Entity\Scope;
class MyController extends ApiController
{
public function scopeApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('scope', $id, $request, $_format, 'scope', Scope::class, [ 'groups' => [ 'read' ] ]);
}
}
This will allow to add a scope by his id, and delete them.
Curl requests:
.. code-block:: bash
# add a scope with id 5
curl -X 'POST' \
'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"type": "scope",
"id": 5
}'
# remove a scope with id 5
curl -X 'DELETE' \
'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 5,
"type": "scope"
}'
Deserializing an association where multiple types are allowed
=============================================================
Sometimes, multiples types are allowed as association to one entity:
.. code-block:: php
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
class Resource
{
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class)
* @ORM\JoinColumn(nullable=true)
*/
private $thirdParty;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=true)
*/
private $person;
/**
*
* @param $resource Person|ThirdParty
*/
public function setResource($resource): self
{
// ...
}
/**
* @return ThirdParty|Person
* @Groups({"read", "write"})
*/
public function getResource()
{
return $this->person ?? $this->thirdParty;
}
}
This is not well taken into account by the Symfony serializer natively.
You must, then, create your own CustomNormalizer. You can help yourself using this:
.. code-block:: php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use Symfony\Component\Serializer\Exception;
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
public function __construct(ResourceRepository $repository)
{
$this->repository = $repository;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// .. snipped for brevity
if ($resource === NULL) {
$resource = new Resource();
}
if (\array_key_exists('resource', $data)) {
$res = $this->denormalizer->denormalize(
$data['resource'],
// call for a "multiple type"
DiscriminatedObjectDenormalizer::TYPE,
$format,
// into the context, we add the list of allowed types:
[
DiscriminatedObjectDenormalizer::ALLOWED_TYPES =>
[
Person::class, ThirdParty::class
]
]
);
$resource->setResource($res);
}
return $resource;
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return $type === Resource::class;
}
}
Serialization for collection
****************************
A specific model has been defined for returning collection:
.. 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
}
}
Where this is relevant, this model should be re-used in custom controller actions.
In custom actions, this can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination <pagination-ref>`).
.. code-block:: php
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

View File

@@ -16,7 +16,6 @@ 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>

View File

@@ -7,8 +7,6 @@
Free Documentation License".
.. _pagination-ref:
Pagination
##########
@@ -17,7 +15,7 @@ The Bundle :code:`Chill\MainBundle` provides a **Pagination** api which allow yo
A simple example
****************
In the controller, get the :code:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance.
In the controller, get the :class:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance.
.. literalinclude:: pagination/example.php

View File

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

View File

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

View File

@@ -1,77 +0,0 @@
<?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);
}
}

View File

@@ -1,237 +0,0 @@
<?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\Validator\Validator\ValidatorInterface;
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
* @throw Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the object is not found
*/
protected function getEntity($action, $id, Request $request): object
{
$e = $this->getDoctrine()
->getRepository($this->getEntityClass())
->find($id);
if (NULL === $e) {
throw $this->createNotFoundException(sprintf("The object %s for id %s is not found", $this->getEntityClass(), $id));
}
return $e;
}
/**
* 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');
}
protected function getValidator(): ValidatorInterface
{
return $this->get('validator');
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,6 @@ 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
@@ -35,34 +32,24 @@ use Chill\MainBundle\CRUD\Controller\CRUDController;
*/
class CRUDRoutesLoader extends Loader
{
protected array $crudConfig = [];
protected array $apiCrudConfig = [];
/**
* @var array
*/
protected $config = [];
/**
* @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 $crudConfig the config from cruds
* @param $apicrudConfig the config from api_crud
* @param $config
*/
public function __construct(array $crudConfig, array $apiConfig)
public function __construct($config)
{
$this->crudConfig = $crudConfig;
$this->apiConfig = $apiConfig;
$this->config = $config;
}
/**
@@ -76,161 +63,53 @@ class CRUDRoutesLoader extends Loader
}
/**
* Load routes for CRUD and CRUD Api
* @return RouteCollection
*/
public function load($resource, $type = null): RouteCollection
public function load($resource, $type = null)
{
if (true === $this->isLoaded) {
throw new \RuntimeException('Do not add the "CRUD" loader twice');
}
$collection = new RouteCollection();
foreach ($this->crudConfig as $crudConfig) {
$collection->addCollection($this->loadCrudConfig($crudConfig));
foreach ($this->config as $config) {
$collection->addCollection($this->loadConfig($config));
}
foreach ($this->apiConfig as $crudConfig) {
$collection->addCollection($this->loadApi($crudConfig));
}
return $collection;
}
/**
* Load routes for CRUD (without api)
*
* @param $crudConfig
* @param $config
* @return RouteCollection
*/
protected function loadCrudConfig($crudConfig): RouteCollection
protected function loadConfig($config): RouteCollection
{
$collection = new RouteCollection();
$controller ='cscrud_'.$crudConfig['name'].'_controller';
foreach ($crudConfig['actions'] as $name => $action) {
// defaults (controller name)
foreach ($config['actions'] as $name => $action) {
$defaults = [
'_controller' => $controller.':'.($action['controller_action'] ?? $name)
'_controller' => 'cscrud_'.$config['name'].'_controller'.':'.($action['controller_action'] ?? $name)
];
if ($name === 'index') {
$path = "{_locale}".$crudConfig['base_path'];
$path = "{_locale}".$config['base_path'];
$route = new Route($path, $defaults);
} elseif ($name === 'new') {
$path = "{_locale}".$crudConfig['base_path'].'/'.$name;
$path = "{_locale}".$config['base_path'].'/'.$name;
$route = new Route($path, $defaults);
} else {
$path = "{_locale}".$crudConfig['base_path'].($action['path'] ?? '/{id}/'.$name);
$path = "{_locale}".$config['base_path'].($action['path'] ?? '/{id}/'.$name);
$requirements = $action['requirements'] ?? [
'{id}' => '\d+'
];
$route = new Route($path, $defaults, $requirements);
}
$collection->add('chill_crud_'.$crudConfig['name'].'_'.$name, $route);
$collection->add('chill_crud_'.$config['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;
}
}

View File

@@ -14,7 +14,6 @@ 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;
@@ -34,6 +33,5 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new ACLFlagsCompilerPass());
$container->addCompilerPass(new GroupingCenterCompilerPass());
$container->addCompilerPass(new RenderEntityCompilerPass());
$container->addCompilerPass(new CRUDControllerCompilerPass());
}
}

View File

@@ -1,63 +0,0 @@
<?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');
}
}
}

View File

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

View File

@@ -1,95 +0,0 @@
<?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();
}
}

View File

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

View File

@@ -35,7 +35,6 @@ use Chill\MainBundle\Doctrine\DQL\OverlapsI;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Chill\MainBundle\Doctrine\DQL\Replace;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ChillMainExtension
@@ -134,7 +133,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/search.yaml');
$loader->load('services/serializer.yaml');
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
$this->configureCruds($container, $config['cruds'], $loader);
}
/**
@@ -189,12 +188,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->prependExtensionConfig('doctrine', array(
'dbal' => [
'types' => [
'dateinterval' => [
'class' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class
],
'point' => [
'class' => \Chill\MainBundle\Doctrine\Type\PointType::class
]
'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class
]
]
));
@@ -213,123 +207,54 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->prependExtensionConfig('monolog', array(
'channels' => array('chill')
));
//add crud api
$this->prependCruds($container);
}
/**
* Load parameter for configuration and set parameters for api
* @param ContainerBuilder $container
* @param array $config the config under 'cruds' key
* @return null
*/
protected function configureCruds(
ContainerBuilder $container,
array $crudConfig,
array $apiConfig,
Loader\YamlFileLoader $loader
): void
protected function configureCruds(ContainerBuilder $container, $config, Loader\YamlFileLoader $loader)
{
if (count($crudConfig) === 0) {
if (count($config) === 0) {
return;
}
$loader->load('services/crud.yaml');
$container->setParameter('chill_main_crud_route_loader_config', $crudConfig);
$container->setParameter('chill_main_api_route_loader_config', $apiConfig);
$container->setParameter('chill_main_crud_route_loader_config', $config);
// Note: the controller are loaded inside compiler pass
}
$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);
/**
* @param ContainerBuilder $container
*/
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'apis' => [
[
'class' => \Chill\MainBundle\Entity\Address::class,
'name' => 'address',
'base_path' => '/api/1.0/main/address',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_POST => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\AddressReference::class,
'name' => 'address_reference',
'base_path' => '/api/1.0/main/address-reference',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\PostalCode::class,
'name' => 'postal_code',
'base_path' => '/api/1.0/main/postal-code',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
],
[
'class' => \Chill\MainBundle\Entity\Country::class,
'name' => 'country',
'base_path' => '/api/1.0/main/country',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
]
],
]
]
]
]);
$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.'%']);
}
}
}

View File

@@ -8,7 +8,6 @@ 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;
/**
@@ -141,7 +140,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()
@@ -169,80 +168,6 @@ 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()
->booleanNode(Request::METHOD_PATCH)->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()
->scalarNode(Request::METHOD_PATCH)->defaultNull()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end() // end of root/children
->end() // end of root
;

View File

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

View File

@@ -1,103 +0,0 @@
<?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;
}
}

View File

@@ -1,27 +0,0 @@
<?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");
}
}

View File

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

View File

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

View File

@@ -1,75 +0,0 @@
<?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;
}
}

View File

@@ -4,8 +4,6 @@ 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
@@ -30,14 +28,14 @@ class Address
*
* @ORM\Column(type="string", length=255)
*/
private $street = '';
private $streetAddress1 = '';
/**
* @var string
*
* @ORM\Column(type="string", length=255)
*/
private $streetNumber = '';
private $streetAddress2 = '';
/**
* @var PostalCode
@@ -45,56 +43,7 @@ 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.
@@ -104,55 +53,27 @@ 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
*
@@ -164,7 +85,7 @@ class Address
}
/**
* Set streetAddress1 (legacy function)
* Set streetAddress1
*
* @param string $streetAddress1
*
@@ -172,23 +93,23 @@ class Address
*/
public function setStreetAddress1($streetAddress1)
{
$this->street = $streetAddress1 === NULL ? '' : $streetAddress1;
$this->streetAddress1 = $streetAddress1 === NULL ? '' : $streetAddress1;
return $this;
}
/**
* Get streetAddress1 (legacy function)
* Get streetAddress1
*
* @return string
*/
public function getStreetAddress1()
{
return $this->street;
return $this->streetAddress1;
}
/**
* Set streetAddress2 (legacy function)
* Set streetAddress2
*
* @param string $streetAddress2
*
@@ -196,19 +117,19 @@ class Address
*/
public function setStreetAddress2($streetAddress2)
{
$this->streetNumber = $streetAddress2 === NULL ? '' : $streetAddress2;
$this->streetAddress2 = $streetAddress2 === NULL ? '' : $streetAddress2;
return $this;
}
/**
* Get streetAddress2 (legacy function)
* Get streetAddress2
*
* @return string
*/
public function getStreetAddress2()
{
return $this->streetNumber;
return $this->streetAddress2;
}
/**
@@ -234,7 +155,7 @@ class Address
{
return $this->postcode;
}
/**
* @return \DateTime
*/
@@ -252,19 +173,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
*/
@@ -275,9 +196,9 @@ class Address
/**
* Set IsNoAddress
*
*
* Indicate true if the address is a fake address (homeless, ...)
*
*
* @param bool $isNoAddress
* @return $this
*/
@@ -286,10 +207,10 @@ class Address
$this->isNoAddress = $isNoAddress;
return $this;
}
/**
* Get customs informations in the address
*
*
* @return array
*/
public function getCustoms(): array
@@ -299,27 +220,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
*/
@@ -331,18 +252,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")
@@ -350,7 +271,7 @@ class Address
->addViolation();
}
}
/**
* @param Address $original
* @return Address
@@ -365,149 +286,5 @@ 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;
}
}

View File

@@ -1,164 +0,0 @@
<?php
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Doctrine\Model\Point;
/**
* @ORM\Entity()
* @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;
}
}

View File

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

View File

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

View File

@@ -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('street', TextType::class, array(
->add('streetAddress1', TextType::class, array(
'required' => !$options['has_no_address'] // true if has no address is false
))
->add('streetNumber', TextType::class, array(
->add('streetAddress2', 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

View File

@@ -1,50 +0,0 @@
<?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()
;
}
*/
}

View File

@@ -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";

View File

@@ -5,7 +5,7 @@
@include button($green, $white);
}
&.bt-reset, &.bt-delete, &.bt-remove {
&.bt-reset, &.bt-delete {
@include button($red, $white);
}
@@ -24,7 +24,6 @@
&.bt-save::before,
&.bt-new::before,
&.bt-delete::before,
&.bt-remove::before,
&.bt-update::before,
&.bt-edit::before,
&.bt-cancel::before,
@@ -57,12 +56,7 @@
// add a trash
content: "";
}
&.bt-remove::before {
// add a times
content: "";
}
&.bt-edit::before, &.bt-update::before {
// add a pencil
content: "";
@@ -100,7 +94,6 @@
&.bt-save::before,
&.bt-new::before,
&.bt-delete::before,
&.bt-remove::before,
&.bt-update::before,
&.bt-edit::before,
&.bt-cancel::before,
@@ -130,7 +123,6 @@
&.bt-save::before,
&.bt-new::before,
&.bt-delete::before,
&.bt-remove::before,
&.bt-update::before,
&.bt-edit::before,
&.bt-cancel::before,

View File

@@ -13,9 +13,9 @@
div#header-accompanying_course-name {
background: none repeat scroll 0 0 #718596;
color: #FFF;
h1 {
margin: 0.4em 0;
}
padding-top: 1em;
padding-bottom: 1em;
span {
a {
color: white;
@@ -39,25 +39,19 @@ div.subheader {
height: 130px;
}
//// SCRATCH BUTTONS
.sc-button {
&.disabled {
cursor: default;
&.bt-remove {
background-color: #d9d9d9;
}
}
}
//// à ranger
.discret {
color: grey;
margin-right: 1em;
}
table {
ul.record_actions {
margin: 0;
padding: 0.5em;
}
div.vue-component {
padding: 1.5em;
margin: 2em 0;
border: 2px dashed grey;
position: relative;
&:before {
content: "vuejs component";
position: absolute;
left: 1.5em;
top: -0.9em;
background-color: white;
color: grey;
padding: 0 0.3em;
}
dd { margin-left: 1em; }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,80 +0,0 @@
<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="body-head">
<slot name="body-head"></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
* and 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>
<style lang="scss">
.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);
}
</style>

View File

@@ -1,59 +0,0 @@
import { createI18n } from 'vue-i18n'
const datetimeFormats = {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
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 }

View File

@@ -6,8 +6,8 @@
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% endif %}
<div class="chill_address_address">
{% 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.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.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>

View File

@@ -1,4 +1,4 @@
<footer class="footer">
<p>{{ 'This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>'|trans|raw }}
<br/> <a name="bottom" href="https://{{ app.request.locale }}.wikibooks.org/wiki/Chill" target="_blank">{{ 'User manual'|trans }}</a></p>
</footer>
<br/> <a href="https://{{ app.request.locale }}.wikibooks.org/wiki/Chill" target="_blank">{{ 'User manual'|trans }}</a></p>
</footer>

View File

@@ -1 +1 @@
<img name="top" class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}">
<img class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}">

View File

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

View File

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

View File

@@ -32,34 +32,34 @@ use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\RoleScope;
/**
* Helper for authorizations.
*
* Helper for authorizations.
*
* Provides methods for user and entities information.
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AuthorizationHelper
class AuthorizationHelper implements AuthorizationHelperInterface
{
/**
*
* @var RoleHierarchyInterface
*/
protected $roleHierarchy;
/**
* The role in a hierarchy, given by the parameter
* The role in a hierarchy, given by the parameter
* `security.role_hierarchy.roles` from the container.
*
* @var string[]
*/
protected $hierarchy;
/**
*
* @var EntityManagerInterface
*/
protected $em;
public function __construct(
RoleHierarchyInterface $roleHierarchy,
$hierarchy,
@@ -69,10 +69,10 @@ class AuthorizationHelper
$this->hierarchy = $hierarchy;
$this->em = $em;
}
/**
* Determines if a user is active on this center
*
*
* @param User $user
* @param Center $center
* @return bool
@@ -81,21 +81,21 @@ class AuthorizationHelper
{
foreach ($user->getGroupCenters() as $groupCenter) {
if ($center->getId() === $groupCenter->getCenter()->getId()) {
return true;
}
}
return false;
}
/**
*
*
* Determines if the user has access to the given entity.
*
*
* if the entity implements Chill\MainBundle\Entity\HasScopeInterface,
* the scope is taken into account.
*
*
* @param User $user
* @param HasCenterInterface $entity the entity may also implement HasScopeInterface
* @param string|Role $attribute
@@ -103,15 +103,15 @@ class AuthorizationHelper
*/
public function userHasAccess(User $user, HasCenterInterface $entity, $attribute)
{
$center = $entity->getCenter();
if (!$this->userCanReachCenter($user, $center)) {
return false;
}
$role = ($attribute instanceof Role) ? $attribute : new Role($attribute);
foreach ($user->getGroupCenters() as $groupCenter){
//filter on center
if ($groupCenter->getCenter()->getId() === $entity->getCenter()->getId()) {
@@ -119,7 +119,7 @@ class AuthorizationHelper
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role allow to reach the required role
if ($this->isRoleReached($role,
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))){
//if yes, we have a right on something...
// perform check on scope if necessary
@@ -137,17 +137,17 @@ class AuthorizationHelper
}
}
}
}
}
return false;
}
/**
* Get reachable Centers for the given user, role,
* and optionnaly Scope
*
*
* @param User $user
* @param Role $role
* @param null|Scope $scope
@@ -156,13 +156,13 @@ class AuthorizationHelper
public function getReachableCenters(User $user, Role $role, Scope $scope = null)
{
$centers = array();
foreach ($user->getGroupCenters() as $groupCenter){
$permissionGroup = $groupCenter->getPermissionsGroup();
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role is in the reachable roles
if ($this->isRoleReached($role,
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))) {
if ($scope === null) {
$centers[] = $groupCenter->getCenter();
@@ -171,19 +171,19 @@ class AuthorizationHelper
if ($scope->getId() == $roleScope->getScope()->getId()){
$centers[] = $groupCenter->getCenter();
break 1;
}
}
}
}
}
}
return $centers;
}
/**
* Return all reachable scope for a given user, center and role
*
*
* @deprecated Use getReachableCircles
*
* @param User $user
@@ -195,10 +195,10 @@ class AuthorizationHelper
{
return $this->getReachableCircles($user, $role, $center);
}
/**
* Return all reachable circle for a given user, center and role
*
*
* @param User $user
* @param Role $role
* @param Center $center
@@ -207,7 +207,7 @@ class AuthorizationHelper
public function getReachableCircles(User $user, Role $role, Center $center)
{
$scopes = array();
foreach ($user->getGroupCenters() as $groupCenter){
if ($center->getId() === $groupCenter->getCenter()->getId()) {
//iterate on permissionGroup
@@ -215,7 +215,7 @@ class AuthorizationHelper
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role is in the reachable roles
if ($this->isRoleReached($role,
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))) {
$scopes[] = $roleScope->getScope();
@@ -223,12 +223,12 @@ class AuthorizationHelper
}
}
}
return $scopes;
}
/**
*
*
* @param Role $role
* @param Center $center
* @param Scope $circle
@@ -239,7 +239,7 @@ class AuthorizationHelper
$parents = $this->getParentRoles($role);
$parents[] = $role;
$parentRolesString = \array_map(function(Role $r) { return $r->getRole(); }, $parents);
$qb = $this->em->createQueryBuilder();
$qb
->select('u')
@@ -250,21 +250,21 @@ class AuthorizationHelper
->where('gc.center = :center')
->andWhere($qb->expr()->in('rs.role', $parentRolesString))
;
$qb->setParameter('center', $center);
if ($circle !== null) {
$qb->andWhere('rs.scope = :circle')
->setParameter('circle', $circle)
;
}
return $qb->getQuery()->getResult();
}
/**
* Test if a parent role may give access to a given child role
*
*
* @param Role $childRole The role we want to test if he is reachable
* @param Role $parentRole The role which should give access to $childRole
* @return boolean true if the child role is granted by parent role
@@ -273,14 +273,14 @@ class AuthorizationHelper
{
$reachableRoles = $this->roleHierarchy
->getReachableRoles([$parentRole]);
return in_array($childRole, $reachableRoles);
}
/**
* Return all the role which give access to the given role. Only the role
* Return all the role which give access to the given role. Only the role
* which are registered into Chill are taken into account.
*
*
* @param Role $role
* @return Role[] the role which give access to the given $role
*/
@@ -291,18 +291,18 @@ class AuthorizationHelper
$roles = \array_map(
function($string) {
return new Role($string);
},
},
\array_keys($this->hierarchy)
);
foreach ($roles as $r) {
$childRoles = $this->roleHierarchy->getReachableRoleNames([$r->getRole()]);
if (\in_array($role, $childRoles)) {
$parentRoles[] = $r;
}
}
return $parentRoles;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\Entity\Scope;
interface AuthorizationHelperInterface
{
/**
* Determines if a user is active on this center
*
* @param User $user
* @param Center $center
* @return bool
*/
public function userCanReachCenter(User $user, Center $center);
/**
*
* Determines if the user has access to the given entity.
*
* if the entity implements Chill\MainBundle\Entity\HasScopeInterface,
* the scope is taken into account.
*
* @param User $user
* @param HasCenterInterface $entity the entity may also implement HasScopeInterface
* @param string|Role $attribute
* @return boolean true if the user has access
*/
public function userHasAccess(User $user, HasCenterInterface $entity, $attribute);
/**
* Get reachable Centers for the given user, role,
* and optionnaly Scope
*
* @param User $user
* @param Role $role
* @param null|Scope $scope
* @return Center[]
*/
public function getReachableCenters(User $user, Role $role, Scope $scope = null);
/**
* Return all reachable scope for a given user, center and role
*
* @deprecated Use getReachableCircles
*
* @param User $user
* @param Role $role
* @param Center $center
* @return Scope[]
*/
public function getReachableScopes(User $user, Role $role, Center $center);
/**
* Return all reachable circle for a given user, center and role
*
* @param User $user
* @param Role $role
* @param Center $center
* @return Scope[]
*/
public function getReachableCircles(User $user, Role $role, Center $center);
/**
*
* @param Role $role
* @param Center $center
* @param Scope $circle
* @return Users
*/
public function findUsersReaching(Role $role, Center $center, Scope $circle = null);
/**
* Return all the role which give access to the given role. Only the role
* which are registered into Chill are taken into account.
*
* @param Role $role
* @return Role[] the role which give access to the given $role
*/
public function getParentRoles(Role $role);
}

View File

@@ -1,28 +0,0 @@
<?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;
}
}

View File

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

View File

@@ -1,41 +0,0 @@
<?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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
/*
* Chill is a software for social workers
*
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS,
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS,
* <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -28,18 +28,18 @@ use Twig\TwigFilter;
*
* @package Chill\MainBundle\Templating\Entity
*/
class ChillEntityRenderExtension extends AbstractExtension
class ChillEntityRenderExtension extends AbstractExtension implements ChillEntityRenderExtensionInterface
{
/**
* @var ChillEntityRenderInterface
* @var ChillEntityRenderInterface
*/
protected $renders = [];
/**
* @var ChillEntityRender
*/
protected $defaultRender;
/**
* ChillEntityRenderExtension constructor.
*/
@@ -47,7 +47,7 @@ class ChillEntityRenderExtension extends AbstractExtension
{
$this->defaultRender = new ChillEntityRender();
}
/**
* @return array|TwigFilter[]
*/
@@ -62,7 +62,7 @@ class ChillEntityRenderExtension extends AbstractExtension
])
];
}
/**
* @param $entity
* @param array $options
@@ -76,7 +76,7 @@ class ChillEntityRenderExtension extends AbstractExtension
return $this->getRender($entity, $options)
->renderString($entity, $options);
}
/**
* @param $entity
* @param array $options
@@ -90,7 +90,7 @@ class ChillEntityRenderExtension extends AbstractExtension
return $this->getRender($entity, $options)
->renderBox($entity, $options);
}
/**
* @param ChillEntityRenderInterface $render
*/
@@ -98,7 +98,7 @@ class ChillEntityRenderExtension extends AbstractExtension
{
$this->renders[] = $render;
}
/**
* @param $entity
* @param $options

View File

@@ -0,0 +1,33 @@
<?php
namespace Chill\MainBundle\Templating\Entity;
use Twig\Extension\ExtensionInterface;
use Twig\TwigFilter;
interface ChillEntityRenderExtensionInterface extends ExtensionInterface
{
/**
* @return array|TwigFilter[]
*/
public function getFilters();
/**
* @param $entity
* @param array $options
* @return string
*/
public function renderString($entity, array $options = []): string;
/**
* @param $entity
* @param array $options
* @return string
*/
public function renderBox($entity, array $options = []): string;
/**
* @param ChillEntityRenderInterface $render
*/
public function addRender(ChillEntityRenderInterface $render);
}

View File

@@ -23,36 +23,36 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Translation\Translator;
/**
*
*
* This helper helps to find the string in current locale from translatable_strings
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*
*/
class TranslatableStringHelper
class TranslatableStringHelper implements TranslatableStringHelperInterface
{
/**
*
*
* @var RequestStack
*/
private $requestStack;
private $fallbackLocales;
public function __construct(RequestStack $requestStack, Translator $translator)
{
$this->requestStack = $requestStack;
$this->fallbackLocales = $translator->getFallbackLocales();
}
/**
* return the string in current locale if it exists.
*
*
* If it does not exists; return the name in the first language available.
*
*
* Return a blank string if any strings are available.
* Return NULL if $translatableString is NULL
*
*
* @param array $translatableStrings
* @return string
*/
@@ -61,32 +61,32 @@ class TranslatableStringHelper
if (NULL === $translatableStrings) {
return NULL;
}
$language = $this->requestStack->getCurrentRequest()->getLocale();
if (isset($translatableStrings[$language])) {
return $translatableStrings[$language];
} else {
foreach ($this->fallbackLocales as $locale) {
if (array_key_exists($locale, $translatableStrings)) {
return $translatableStrings[$locale];
}
}
}
// no fallback translation... trying the first available
$langs = array_keys($translatableStrings);
if (count($langs) === 0) {
return '';
}
return $translatableStrings[$langs[0]];
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Chill\MainBundle\Templating;
interface TranslatableStringHelperInterface
{
/**
* return the string in current locale if it exists.
*
* If it does not exists; return the name in the first language available.
*
* Return a blank string if any strings are available.
* Return NULL if $translatableString is NULL
*
* @param array $translatableStrings
* @return string
*/
public function localize(array $translatableStrings);
}

View File

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

View File

@@ -1,119 +0,0 @@
<?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);
}
}

View File

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

View File

@@ -29,40 +29,40 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface;
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class TimelineBuilder implements ContainerAwareInterface
class TimelineBuilder implements ContainerAwareInterface, TimelineBuilderInterface
{
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
*
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
/**
* Record provider
*
*
* This array has the structure `[ 'service id' => $service ]`
*
* @var TimelineProviderInterface[]
*/
private $providers = [];
/**
* Record provider and their context
*
*
* This array has the structure `[ 'context' => [ 'service id' ] ]`
*
* @var array
*/
private $providersByContext = [];
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* return an HTML string with timeline
*
@@ -79,18 +79,18 @@ class TimelineBuilder implements ContainerAwareInterface
public function getTimelineHTML($context, array $args, $firstItem = 0, $number = 20)
{
$union = $this->buildUnionQuery($context, $args);
//add ORDER BY clause and LIMIT
$query = $union . sprintf(' ORDER BY date DESC LIMIT %d OFFSET %d',
$number, $firstItem);
// run query and handle results
$fetched = $this->runUnionQuery($query);
$entitiesByKey = $this->getEntities($fetched, $context);
return $this->render($fetched, $entitiesByKey, $context, $args);
}
/**
* Return the number of items for the given context and args
*
@@ -101,17 +101,17 @@ class TimelineBuilder implements ContainerAwareInterface
public function countItems($context, array $args)
{
$union = $this->buildUnionQuery($context, $args);
// embed the union query inside a count query
$count = sprintf('SELECT COUNT(sq.id) AS total FROM (%s) as sq', $union);
$rsm = (new ResultSetMapping())
->addScalarResult('total', 'total', Type::INTEGER);
return $this->em->createNativeQuery($count, $rsm)
->getSingleScalarResult();
}
/**
* add a provider id
*
@@ -125,7 +125,7 @@ class TimelineBuilder implements ContainerAwareInterface
$this->providersByContext[$context][] = $id;
$this->providers[$id] = $provider;
}
/**
* Get providers by context
*
@@ -139,16 +139,16 @@ class TimelineBuilder implements ContainerAwareInterface
throw new \LogicException(sprintf('No builders have been defined for "%s"'
. ' context', $context));
}
$providers = [];
foreach($this->providersByContext[$context] as $providerId) {
$providers[] = $this->providers[$providerId];
}
return $providers;
}
/**
* build the UNION query with all providers
*
@@ -170,10 +170,10 @@ class TimelineBuilder implements ContainerAwareInterface
$append = ($union === '') ? $select : ' UNION '.$select;
$union .= $append;
}
return $union;
}
/**
* return the SQL SELECT query as a string,
*
@@ -186,7 +186,7 @@ class TimelineBuilder implements ContainerAwareInterface
private function buildSelectQuery(TimelineProviderInterface $provider, $context, array $args)
{
$data = $provider->fetchQuery($context, $args);
return sprintf(
'SELECT %s AS id, '
. '%s AS "date", '
@@ -199,7 +199,7 @@ class TimelineBuilder implements ContainerAwareInterface
$data['FROM'],
$data['WHERE']);
}
/**
* run the UNION query and return result as an array
*
@@ -212,11 +212,11 @@ class TimelineBuilder implements ContainerAwareInterface
->addScalarResult('id', 'id')
->addScalarResult('type', 'type')
->addScalarResult('date', 'date');
return $this->em->createNativeQuery($query, $resultSetMapping)
->getArrayResult();
}
/**
*
* @param array $queriedIds
@@ -227,11 +227,11 @@ class TimelineBuilder implements ContainerAwareInterface
{
//gather entities by type to pass all id with same type to the TimelineProvider.
$idsByType = array();
foreach($queriedIds as $result) {
$idsByType[$result['type']][] = $result['id'];
}
//fetch entities from providers
$entitiesByType = array();
foreach ($idsByType as $type => $ids) {
@@ -243,10 +243,10 @@ class TimelineBuilder implements ContainerAwareInterface
}
}
}
return $entitiesByType;
}
/**
* render the timeline as HTML
*
@@ -269,17 +269,17 @@ class TimelineBuilder implements ContainerAwareInterface
$timelineEntry['date'] = new \DateTime($result['date']);
$timelineEntry['template'] = $data['template'];
$timelineEntry['template_data'] = $data['template_data'];
$timelineEntries[] = $timelineEntry;
}
return $this->container->get('templating')
->render('@ChillMain/Timeline/index.html.twig', array(
'results' => $timelineEntries
));
}
/**
* get the template data from the provider for the given entity, by type.
*

View File

@@ -0,0 +1,24 @@
<?php
/*
* Copyright (C) 2015 Champs-Libres Coopérative <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\Timeline;
interface TimelineBuilderInterface
{
}

View File

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

View File

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

View File

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

View File

@@ -3,18 +3,6 @@ parameters:
services:
Chill\MainBundle\Serializer\Normalizer\:
resource: '../Serializer/Normalizer'
autowire: true
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Doctrine\Event\:
resource: '../Doctrine/Event/'
autowire: true
tags:
- { name: 'doctrine.event_subscriber' }
chill.main.helper.translatable_string:
class: Chill\MainBundle\Templating\TranslatableStringHelper
arguments:
@@ -22,6 +10,7 @@ services:
- "@translator.default"
Chill\MainBundle\Templating\TranslatableStringHelper: '@chill.main.helper.translatable_string'
Chill\MainBundle\Templating\TranslatableStringHelperInterface: Chill\MainBundle\Templating\TranslatableStringHelper
chill.main.twig.translatable_string:
class: Chill\MainBundle\Templating\TranslatableStringTwig
@@ -31,14 +20,14 @@ services:
- [ setContainer, ["@service_container"]]
tags:
- { name: twig.extension }
chill.main.twig.widget:
class: Chill\MainBundle\Templating\Widget\WidgetRenderingTwig
arguments:
- "@event_dispatcher"
tags:
- { name: twig.extension }
chill.main.twig.csv_cell:
class: Chill\MainBundle\Templating\CSVCellTwig
tags:

View File

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

View File

@@ -1,8 +1,7 @@
services:
Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader:
arguments:
$crudConfig: '%chill_main_crud_route_loader_config%'
$apiConfig: '%chill_main_api_route_loader_config%'
$config: '%chill_main_crud_route_loader_config%'
tags: [ routing.loader ]
Chill\MainBundle\CRUD\Resolver\Resolver:
@@ -14,4 +13,4 @@ services:
arguments:
$resolver: '@Chill\MainBundle\CRUD\Resolver\Resolver'
tags:
- { name: twig.extension }
- { name: twig.extension }

View File

@@ -6,7 +6,6 @@ services:
- "@request_stack"
- "@router"
- "%chill_main.pagination.item_per_page%"
Chill\MainBundle\Pagination\PaginatorFactory: '@chill_main.paginator_factory'
chill_main.paginator.twig_extensions:

View File

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

View File

@@ -1,4 +1,4 @@
services:
services:
chill.main.security.authorization.helper:
class: Chill\MainBundle\Security\Authorization\AuthorizationHelper
arguments:
@@ -6,47 +6,48 @@ services:
$hierarchy: "%security.role_hierarchy.roles%"
$em: '@Doctrine\ORM\EntityManagerInterface'
Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper'
Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface: Chill\MainBundle\Security\Authorization\AuthorizationHelper
chill.main.role_provider:
class: Chill\MainBundle\Security\RoleProvider
chill.main.user_provider:
class: Chill\MainBundle\Security\UserProvider\UserProvider
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
Chill\MainBundle\Security\Authorization\ChillExportVoter:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
tags:
- { name: security.voter }
Chill\MainBundle\Security\PasswordRecover\TokenManager:
Chill\MainBundle\Security\PasswordRecover\TokenManager:
arguments:
$secret: '%kernel.secret%'
$logger: '@Psr\Log\LoggerInterface'
Chill\MainBundle\Security\PasswordRecover\RecoverPasswordHelper:
arguments:
$tokenManager: '@Chill\MainBundle\Security\PasswordRecover\TokenManager'
$urlGenerator: '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
$mailer: '@Chill\MainBundle\Notification\Mailer'
$routeParameters: "%chill_main.notifications%"
Chill\MainBundle\Security\PasswordRecover\PasswordRecoverEventSubscriber:
arguments:
$locker: '@Chill\MainBundle\Security\PasswordRecover\PasswordRecoverLocker'
tags:
- { name: kernel.event_subscriber }
Chill\MainBundle\Security\PasswordRecover\PasswordRecoverLocker:
arguments:
$chillRedis: '@Chill\MainBundle\Redis\ChillRedis'
$logger: '@Psr\Log\LoggerInterface'
Chill\MainBundle\Security\PasswordRecover\PasswordRecoverVoter:
arguments:
$locker: '@Chill\MainBundle\Security\PasswordRecover\PasswordRecoverLocker'
$requestStack: '@Symfony\Component\HttpFoundation\RequestStack'
tags:
- { name: security.voter }
- { name: security.voter }

View File

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

View File

@@ -30,6 +30,7 @@ services:
Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension:
tags:
- { name: twig.extension }
Chill\MainBundle\Templating\Entity\ChillEntityRenderExtensionInterface: Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension
Chill\MainBundle\Templating\Entity\CommentRender:
arguments:
@@ -37,7 +38,7 @@ services:
- '@Symfony\Component\Templating\EngineInterface'
tags:
- { name: 'chill.render_entity' }
Chill\MainBundle\Templating\ChillMarkdownRenderExtension:
tags:
- { name: twig.extension }

View File

@@ -4,4 +4,6 @@ services:
arguments:
- "@doctrine.orm.entity_manager"
calls:
- [ setContainer, ["@service_container"]]
- [ setContainer, ["@service_container"]]
Chill\MainBundle\Timeline\TimelineBuilderInterface: "@chill_main.timeline_builder"

View File

@@ -1,32 +0,0 @@
<?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";
}
}

View File

@@ -1,50 +0,0 @@
<?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');
}
}

View File

@@ -1,33 +0,0 @@
<?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');
}
}

View File

@@ -1,58 +0,0 @@
<?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;
');
}
}

View File

@@ -1,175 +0,0 @@
<?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;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Entity\Scope;
use Symfony\Component\Workflow\Registry;
class AccompanyingCourseApiController extends ApiController
{
protected EventDispatcherInterface $eventDispatcher;
protected ValidatorInterface $validator;
private Registry $registry;
public function __construct(
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator,
Registry $registry
) {
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
$this->registry = $registry;
}
public function confirmApi($id, Request $request, $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
$this->checkACL('confirm', $request, $_format, $accompanyingPeriod);
$workflow = $this->registry->get($accompanyingPeriod);
if (FALSE === $workflow->can($accompanyingPeriod, 'confirm')) {
throw new BadRequestException('It is not possible to confirm this period');
}
$workflow->apply($accompanyingPeriod, 'confirm');
$this->getDoctrine()->getManager()->flush();
return $this->json($accompanyingPeriod, Response::HTTP_OK, [], [
'groups' => [ 'read' ]
]);
}
public function participationApi($id, Request $request, $_format)
{
/** @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);
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, 422);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($participation, 200, [], ['groups' => [ 'read' ]]);
}
public function resourceApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('resource', $id, $request, $_format, 'resource', Resource::class);
}
public function scopeApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('scope', $id, $request, $_format, 'scope', Scope::class, [ 'groups' => [ 'read' ] ]);
}
public function commentApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('comment', $id, $request, $_format, 'comment', Comment::class);
}
public function socialIssueApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('socialissue', $id, $request, $_format, 'socialIssue', SocialIssue::class, [ 'groups' => [ 'read' ] ]);
}
public function requestorApi($id, Request $request, string $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$action = 'requestor';
$accompanyingPeriod = $this->getEntity($action, $id, $request);
// a requestor may be a person or a thirdParty
$this->checkACL($action, $request, $_format, $accompanyingPeriod);
$this->onPostCheckACL($action, $request, $_format, $accompanyingPeriod);
if (Request::METHOD_DELETE === $request->getMethod()) {
$accompanyingPeriod->setRequestor(NULL);
} elseif (Request::METHOD_POST === $request->getMethod()) {
$requestor = null;
$exceptions = [];
foreach ([Person::class, ThirdParty::class] as $class) {
try {
$requestor = $this->getSerializer()
->deserialize($request->getContent(), $class, $_format, []);
} catch (RuntimeException $e) {
$exceptions[] = $e;
}
}
if ($requestor === null) {
throw new BadRequestException('Could not find any person or requestor', 0, $exceptions[0]);
}
$accompanyingPeriod->setRequestor($requestor);
} else {
throw new BadRequestException('method not supported');
}
$errors = $this->validator->validate($accompanyingPeriod);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors, 422);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($accompanyingPeriod->getRequestor(), 200, [], ['groups' => [ 'read']]);
}
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response
{
$this->eventDispatcher->dispatch(
AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT,
new AccompanyingPeriodPrivacyEvent($entity, [
'action' => $action,
'request' => $request->getMethod()
])
);
return null;
}
}

View File

@@ -1,17 +0,0 @@
<?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'));
}
}

View File

@@ -1,88 +0,0 @@
<?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();
}
}

View File

@@ -1,62 +0,0 @@
<?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();
}
}

View File

@@ -1,82 +0,0 @@
<?php
/*
* Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
/**
* Create social actions
*
*/
class LoadSocialActions extends AbstractFixture implements OrderedFixtureInterface
{
public function getOrder()
{
return 10020;
}
public static $socialActions = [
'social_action_info_conseil' => [
'title' => [
'fr' => 'Informer, conseiller'
],
'issue' => 'social_issue_prev_prot'
],
'social_action_instruire' => [
'title' => [
'fr' => 'Instruire l\'imprime unique pour des impayés'
],
'issue' => 'social_issue_prev_prot'
],
'social_action_MASP' => [
'title' => [
'fr' => 'MASP'
],
'issue' => 'social_issue_diff_fin'
],
'social_action_protection_enfant' => [
'title' => [
'fr' => 'Protection Enfant confié dans le cadre judiciaire'
],
'issue' => 'social_issue_enfant_protection'
],
];
public function load(ObjectManager $manager)
{
foreach (static::$socialActions as $ref => $new) {
$socialAction = new SocialAction();
$socialAction->setTitle($new['title']);
$socialAction->setIssue($this->getReference($new['issue']));
$socialAction->setDefaultNotificationDelay(new \DateInterval('P5D'));
$manager->persist($socialAction);
$this->addReference($ref, $socialAction);
print("Adding SocialAction '".$new['title']['fr']."'\n");
}
$manager->flush();
}
}

View File

@@ -1,70 +0,0 @@
<?php
/*
* Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Chill\PersonBundle\Entity\SocialWork\Goal;
/**
* Create social goals
*
*/
class LoadSocialGoals extends AbstractFixture implements OrderedFixtureInterface
{
public function getOrder()
{
return 10030;
}
public static $socialGoals = [
'social_goal_instuire_dossier' => [
'title' => [
'fr' => 'Instruire le dossier de surendettement'
],
'action' => 'social_action_MASP'
],
'social_goal_proteger' => [
'title' => [
'fr' => 'Protéger via une assistance educative placement'
],
'action' => 'social_action_protection_enfant'
],
];
public function load(ObjectManager $manager)
{
foreach (static::$socialGoals as $ref => $new) {
$socialGoal = new Goal();
$socialGoal->setTitle($new['title']);
$socialGoal->addSocialAction($this->getReference($new['action']));
$manager->persist($socialGoal);
$this->addReference($ref, $socialGoal);
print("Adding SocialGoal '".$new['title']['fr']."'\n");
}
$manager->flush();
}
}

View File

@@ -1,90 +0,0 @@
<?php
/*
* Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
/**
* Create social issues
*
*/
class LoadSocialIssues extends AbstractFixture implements OrderedFixtureInterface
{
public function getOrder()
{
return 10010;
}
public static $socialIssues = [
'social_issue_diff_fin_or_admin' => [
'title' => [
'fr' => 'ADULTE - DIFFICULTES FINANCIERES ET/OU ADMINISTRATIVES'
]
],
'social_issue_prev_prot' => [
'title' => [
'fr' => 'ADULTE PREVENTION/PROTECTION'
],
'parent' => 'social_issue_diff_fin_or_admin'
],
'social_issue_diff_fin' => [
'title' => [
'fr' => 'Difficulté financière'
],
'parent' => 'social_issue_diff_fin_or_admin'
],
'social_issue_enfant_famille' => [
'title' => [
'fr' => 'Enfant / famille'
]
],
'social_issue_enfant_protection' => [
'title' => [
'fr' => 'enfant - protection'
],
'parent' => 'social_issue_enfant_famille'
],
];
public function load(ObjectManager $manager)
{
foreach (static::$socialIssues as $ref => $new) {
$socialIssue = new SocialIssue();
$socialIssue->setTitle($new['title']);
if ( array_key_exists('parent', $new)) {
$parentRef = $new['parent'];
$parent = $this->getReference($parentRef);
$socialIssue->setParent($parent);
}
$manager->persist($socialIssue);
$this->addReference($ref, $socialIssue);
print("Adding SocialIssue '".$new['title']['fr']."'\n");
}
$manager->flush();
}
}

View File

@@ -1,94 +0,0 @@
<?php
/*
* Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Chill\PersonBundle\Entity\SocialWork\Result;
/**
* Create social results
*
*/
class LoadSocialResults extends AbstractFixture implements OrderedFixtureInterface
{
public function getOrder()
{
return 10040;
}
public static $socialResults = [
'social_result_FSL_acces' => [
'title' => [
'fr' => 'FSL - accès cautionnement'
],
'action' => 'social_action_instruire'
],
'social_result_FSL_maintien' => [
'title' => [
'fr' => 'FSL maintien - impayés de loyer'
],
'action' => 'social_action_MASP'
],
'social_result_soutien_parental' => [
'title' => [
'fr' => 'Soutien parental'
],
// 'action' => 'social_action_protection_enfant', (via le goal)
'goal' => 'social_goal_proteger'
],
'social_result_accompagnement_mineur' => [
'title' => [
'fr' => 'Accompagnement du mineur'
],
// 'action' => 'social_action_protection_enfant', (via le goal)
'goal' => 'social_goal_proteger',
],
];
public function load(ObjectManager $manager)
{
foreach (static::$socialResults as $ref => $new) {
$socialResult = new Result();
$socialResult->setTitle($new['title']);
if ( array_key_exists('action', $new)) {
$action = $this->getReference($new['action']);
$socialResult->addSocialAction($action);
}
if ( array_key_exists('goal', $new)) {
$goal = $this->getReference($new['goal']);
$socialResult->addGoal($goal);
}
$manager->persist($socialResult);
$this->addReference($ref, $socialResult);
print("Adding SocialResult '".$new['title']['fr']."'\n");
}
$manager->flush();
}
}

View File

@@ -1,69 +0,0 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Chill\MainBundle\Entity\Address;
/**
* @ORM\Entity
*/
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;
}
}

View File

@@ -1,152 +0,0 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
/**
* @ORM\Entity
*/
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;
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMembers;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
/**
* @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)
*/
final class HouseholdMembersRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(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()
;
}
*/
}

View File

@@ -1,52 +0,0 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Household;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
/**
* @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)
*/
final class HouseholdRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(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()
;
}
*/
}

View File

@@ -1,78 +0,0 @@
.chill-blue, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+4) h2 {
color: #334d5c; }
.chill-green, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+2) h2 {
color: #43b29d; }
.chill-green-dark {
color: #328474; }
.chill-yellow {
color: #eec84a; }
.chill-orange, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+3) h2 {
color: #e2793d; }
.chill-red, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+1) h2 {
color: #df4949; }
.chill-gray {
color: #ececec; }
.chill-beige {
color: #cabb9f; }
.chill-pink {
color: #dd506d; }
.chill-dark-gray {
color: #333333; }
.chill-light-gray {
color: #b2b2b2; }
div#header-person-name {
background: none repeat scroll 0 0 #328474;
color: #FFF;
padding-top: 1em;
padding-bottom: 1em; }
div#header-person-details {
background: none repeat scroll 0 0 #43b29d;
color: #FFF;
padding-top: 1em;
padding-bottom: 1em; }
div#person_details_container {
padding-top: 20px;
padding-bottom: 20px; }
div.person-view {
/* custom fields on the home page */ }
div.person-view figure.person-details {
/* background-color: $black;
padding-top: 0.2em;
padding-bottom: 0.2em;
}*/ }
div.person-view figure.person-details h2 {
font-family: 'Open Sans';
font-weight: 600;
margin-bottom: 0.3em;
font-variant: small-caps; }
div.person-view figure.person-details dl {
margin-top: 0.3em; }
div.person-view figure.person-details dt {
font-family: 'Open Sans';
font-weight: 600; }
div.person-view figure.person-details dd {
margin-left: 0; }
div.person-view div.custom-fields figure.person-details {
display: flex;
flex-flow: row wrap; }
div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(2n+1) {
width: 50%;
margin-right: 40px; }
div.person-view div.custom-fields figure.person-details iv.cf_title_box:nth-child(2n+2) {
width: calc(50% - 40px); }
/*# sourceMappingURL=person.css.map */

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