Compare commits

..

3 Commits

Author SHA1 Message Date
3e7c79d6ce test app async working 2021-04-23 12:10:37 +02:00
bc1f624e14 fix dummy controller 2021-04-23 10:49:15 +02:00
f73d3bf320 test app 2021-04-22 20:42:48 +02:00
376 changed files with 2875 additions and 19361 deletions

64
.env
View File

@@ -1,64 +0,0 @@
##
## Manually dump .env files in .env.local.php with
## `$ composer symfony:dump-env prod`
##
## Project environment
APP_ENV=dev
## Enable debug
APP_DEBUG=true
## Locale
LOCALE=fr
## Framework secret
APP_SECRET=ThisTokenIsNotSoSecretChangeIt
## Symfony/swiftmailer
MAILER_TRANSPORT=smtp
MAILER_HOST=smtp
MAILER_PORT=1025
MAILER_CRYPT=
MAILER_AUTH=
MAILER_USER=
MAILER_PASSWORD=
MAILER_URL=${MAILER_TRANSPORT}://${MAILER_HOST}:${MAILER_PORT}?encryption=${MAILER_CRYPT}&auth_mode=${MAILER_AUTH}&username=${MAILER_USER}&password=${MAILER_PASSWORD}
## Notifications
NOTIFICATION_HOST=localhost:8001
NOTIFICATION_FROM_EMAIL=admin@chill.social
NOTIFICATION_FROM_NAME=Chill
## Gelf
GELF_HOST=gelf
GELF_PORT=12201
## OVH OpenStack Storage User/Role
OS_USERNAME=
OS_PASSWORD=
OS_TENANT_ID=
OS_REGION_NAME=GRA
OS_AUTH_URL=https://auth.cloud.ovh.net/v2.0/
## OVH OpenStack Storage Container
ASYNC_UPLOAD_TEMP_URL_KEY=
ASYNC_UPLOAD_TEMP_URL_BASE_PATH=
ASYNC_UPLOAD_TEMP_URL_CONTAINER=
## Redis Cache
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
## Twilio
TWILIO_SID=~
TWILIO_SECRET=~
## DOCKER IMAGES REGISTRY
#IMAGE_PHP=
#IMAGE_NGINX=
## DOCKER IMAGES VERSION
#VERSION=test
VERSION=prod

View File

@@ -1,6 +0,0 @@
# variables for .env environement
# those variables suits for gitlab-ci
# Run tests from root to adapt your own environment
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?serverVersion=12&charset=utf8

18
.gitignore vendored
View File

@@ -1,22 +1,4 @@
.composer/*
composer.phar
composer.lock
docs/build/
.php_cs.cache
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
/bin/
###< symfony/framework-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

View File

@@ -1,42 +0,0 @@
---
image: registry.gitlab.com/chill-projet/chill-app/php-base-image:7.4
# Select what we should cache between builds
cache:
paths:
- tests/app/vendor/
before_script:
# add extensions to postgres
- PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -h db -c "CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Install and run Composer
- curl -sS https://getcomposer.org/installer | php
- php -d memory_limit=2G composer.phar install
- php tests/app/bin/console doctrine:migrations:migrate -n
- php -d memory_limit=2G tests/app/bin/console doctrine:fixtures:load -n
- echo "before_script finished"
# 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
alias: db
- name: redis
alias: redis
# Set any variables we need
variables:
# Configure postgres environment variables (https://hub.docker.com/r/_/postgres/)
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# fetch the chill-app using git submodules
GIT_SUBMODULE_STRATEGY: recursive
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_URL: redis://redis:6379
# Run our tests
test:
script:
- php -d memory_limit=3G bin/phpunit --colors=never

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "_exts/sphinx-php"]
path = _exts/sphinx-php
url = https://github.com/fabpot/sphinx-php.git
[submodule "tests/app"]
path = tests/app
url = https://gitlab.com/Chill-projet/chill-app.git

View File

@@ -1,9 +0,0 @@
# Chill framework
Documentation of the Chill software.
The online documentation can be found at http://docs.chill.social
See the [`docs`][1] directory for more.
[1]: docs/README.md

View File

@@ -1,110 +1,72 @@
{
"name": "chill-project/chill-bundles",
"license": "AGPL-3.0-only",
"type": "library",
"description": "Most used bundles for chill-project",
"keywords": ["chill", "social worker"],
"authors": [
{
"name": "Champs-Libres",
"email": "info@champs-libres.coop",
"homepage": "http://www.champs-libres.coop"
}
],
"type": "library",
"require": {
"components/jquery": ">=1.7.1",
"league/csv": "^9.6",
"phpoffice/phpspreadsheet": "~1.2",
"robloach/component-installer": "*"
},
"config": {
"vendor-dir": "tests/app/vendor",
"bin-dir": "bin"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13",
"nelmio/alice": "^3.8",
"phpunit/phpunit": "^9.5",
"symfony/debug-bundle": "^5.1",
"symfony/dotenv": "^5.1",
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^5.2",
"symfony/stopwatch": "^5.1",
"symfony/var-dumper": "4.*",
"symfony/web-profiler-bundle": "^5.0",
"symplify/monorepo-builder": "^9.2"
},
"autoload": {
"psr-4": {
"Chill\\AMLI\\BudgetBundle\\": "src/Bundle/ChillBudgetBundle/",
"Chill\\AMLI\\FamilyMembersBundle\\": "src/Bundle/ChillFamilyMembersBundle/",
"Chill\\ActivityBundle\\": "src/Bundle/ChillActivityBundle/",
"Chill\\CustomFieldsBundle\\": "src/Bundle/ChillCustomFieldsBundle/",
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle/",
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle/",
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle/",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle/",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle/",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle/",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle/"
"Chill\\ActivityBundle\\": "src/Bundle/ChillActivityBundle",
"Chill\\BudgetBundle\\": "src/Bundle/ChillBudgetBundle",
"Chill\\CustomFieldsBundle\\": "src/Bundle/ChillCustomFieldsBundle",
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
"Chill\\FamilyMemberBundle\\": "src/Bundle/ChillFamilyMemberBundle",
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle"
}
},
"autoload-dev": {
"classmap": [
"src/Bundle/ChillActivityBundle/Resources/test/Fixtures/App/app/AppKernel.php",
"src/Bundle/ChillCustomFieldsBundle/Resources/test/Fixtures/App/app/AppKernel.php",
"src/Bundle/ChillMainBundle/Resources/test/Fixtures/App/app/AppKernel.php",
"src/Bundle/ChillPersonBundle/Resources/test/Fixtures/App/app/AppKernel.php",
"src/Bundle/ChillReportBundle/Resources/test/Fixtures/App/app/AppKernel.php"
]
"require": {
"champs-libres/async-uploader-bundle": "dev-sf4",
"graylog2/gelf-php": "^1.5",
"symfony/form": "4.*",
"symfony/twig-bundle": "^4.4",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"composer/package-versions-deprecated": "^1.10",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.7",
"symfony/asset": "4.*",
"symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "4.*",
"symfony/translation": "4.*",
"symfony/validator": "4.*",
"sensio/framework-extra-bundle": "^5.5",
"symfony/yaml": "4.*",
"symfony/webpack-encore-bundle": "^1.11",
"knplabs/knp-menu": "^3.1",
"knplabs/knp-menu-bundle": "^3.0",
"symfony/templating": "4.*",
"twig/intl-extra": "^3.0",
"symfony/workflow": "4.*",
"symfony/expression-language": "4.*",
"knplabs/knp-time-bundle": "^1.12",
"symfony/intl": "4.*",
"symfony/swiftmailer-bundle": "^3.5",
"league/csv": "^9.6",
"phpoffice/phpspreadsheet": "^1.16",
"symfony/browser-kit": "^5.2",
"symfony/css-selector": "^5.2",
"twig/markdown-extra": "^3.3",
"erusev/parsedown": "^1.7",
"symfony/serializer": "^5.2"
},
"extra": {
"component": {
"scripts": [
"select2.js"
],
"files": [
"select2.js",
"select2_locale_*.js",
"select2.css",
"select2-bootstrap.css",
"select2-spinner.gif",
"select2.png",
"select2x2.png"
]
},
"app-migrations-dir": [
"Resources/test/Fixtures/App/app/DoctrineMigrations",
"Resources/test/Fixtures/App/DoctrineMigrations"
],
"symfony-app-dir": [
"Tests/Fixtures/App/app",
"Tests/Fixtures/App/",
"Test/Fixtures/App/app/"
]
"conflict": {
"symfony/symfony": "*"
},
"scripts": {
"merge": "vendor/bin/monorepo-builder merge --ansi",
"propagate": "vendor/bin/monorepo-builder propagate --ansi",
"validate-monorepo": "vendor/bin/monorepo-builder validate --ansi",
"release": "vendor/bin/monorepo-builder release patch --ansi",
"localize": "vendor/bin/monorepo-builder localize-composer-paths"
},
"replace": {
"chill-project/activity": "self.version",
"chill-project/budget": "self.version",
"chill-project/chill-doc-store": "self.version",
"chill-project/custom-fields": "self.version",
"chill-project/event": "self.version",
"chill-project/family-members": "self.version",
"chill-project/main": "self.version",
"chill-project/person": "self.version",
"chill-project/report": "self.version",
"chill-project/task": "self.version",
"chill-project/third-party": "self.version",
"ivaynberg/select2": "self.version"
},
"minimum-stability": "dev",
"prefer-stable": true
"require-dev": {
"fakerphp/faker": "^1.13",
"phpunit/phpunit": "^7.0",
"symfony/dotenv": "^5.1",
"symfony/maker-bundle": "^1.20",
"doctrine/doctrine-fixtures-bundle": "^3.3",
"symfony/stopwatch": "^5.1",
"symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1"
}
}

View File

@@ -10,19 +10,10 @@ Compilation into HTML
To compile this documentation :
1. Install [sphinx-doc](http://sphinx-doc.org)
``` bash
$ virtualenv .venv # creation of the virtual env (only the first time)
$ source .venv/bin/activate # activate the virtual env
(.venv) $ pip install -r requirements.txt
```
1. Install [sphinx-doc](http://sphinx-doc.org) (eg. pip install sphinx & pip install sphinx_rtd_theme)
2. Install submodules : $ git submodule update --init;
3. run `make html` from the root directory
4. The base file is located on build/html/index.html
``` bash
$ cd build/html
$ python -m http.server 8888 # will serve the site on the port 8888
```
Contribute
===========

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>
@@ -30,7 +29,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
Timelines <timelines.rst>
Exports <exports.rst>
Embeddable comments <embeddable-comments.rst>
Run tests <run-tests.rst>
Testing <make-test-working.rst>
Useful snippets <useful-snippets.rst>
manual/index.rst
Assets <assets.rst>

View File

@@ -0,0 +1,231 @@
.. 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".
Make tests working
******************
Unit and functional tests are important to ensure that bundle may be deployed securely.
In reason of the Chill architecture, test should be runnable from the bundle's directory and works correctly: this will allow continuous integration tools to run tests automatically.
.. note::
Integration tools (i.e. `travis-ci <https://travis-ci.org>`_) works like this :
* they clone the bundle repository in a virtual machine, using git
* they optionnaly run `composer` to download and install depedencies
* they optionnaly run other command to prepare a database, insert fixtures, ...
* they run test
On the developer's machine test should be runnable with two or three commands **runned from the bundle directory** :
.. code-block:: bash
$ composer install --dev
$ // command to insert fixtures, ...
$ phpunit
This chapter has been inspired by `this useful blog post <http://blog.kevingomez.fr/2013/01/09/functional-testing-standalone-symfony2-bundles/>`_.
Bootstrap phpunit for a standalone bundle
==========================================
Unit tests should run after achieving this step.
phpunit.xml
-----------
A `phpunit.xml.dist` file should be present at the bundle root.
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./Tests/bootstrap.php" colors="true">
<!-- the file "./Tests/boostrap.php" will be created on the next step -->
<testsuites>
<testsuite name="ChillMain test suite">
<directory suffix="Test.php">./Tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
bootstrap.php
--------------
A file `boostrap.php`, located in the `Tests` directory, will allow phpunit to resolve class autoloading :
.. code-block:: php
<?php
if (!is_file($autoloadFile = __DIR__.'/../vendor/autoload.php')) {
throw new \LogicException('Could not find autoload.php in vendor/. Did you run "composer install --dev"?');
}
require $autoloadFile;
composer.json
-------------
The `composer.json` file **located at the bundle's root** should contains all depencies needed to run test (and to execute bundle functions).
Ensure that all dependencies are included in the `require` and `require-dev` sections.
Functional tests
================
If you want to access services, database, and run functional tests, you will have to bootstrap a symfony app, with the minimal configuration. Three files are required :
* a `config_test.yml` file (eventually with a `config.yml`);
* a `routing.yml` file
* an `AppKernel.php` file
Adapt phpunit.xml
-----------------
You should add reference to the new application within `phpunit.xml.dist`. The directive `<php>` should be added like this, if your `AppKernel.php` file is located in `Tests/Fixtures/App` directory:
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./Tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="ChillMain test suite">
<directory suffix="Test.php">./Tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
<!-- the lines we added -->
<php>
<server name="KERNEL_DIR" value="./Tests/Fixtures/App/" />
</php>
</phpunit>
AppKernel.php
-------------
This file boostrap the app. It contains three functions. This is the file used in the ChillMain bundle :
.. code-block:: php
<?php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
public function registerBundles()
{
return array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Chill\MainBundle\ChillMainBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new \Symfony\Bundle\AsseticBundle\AsseticBundle(),
#add here all the required bundle (some bundle are not required)
);
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
/**
* @return string
*/
public function getCacheDir()
{
return sys_get_temp_dir().'/ChillMainBundle/cache';
}
/**
* @return string
*/
public function getLogDir()
{
return sys_get_temp_dir().'/ChillMainBundle/logs';
}
}
config_test.yml
---------------
There are only few parameters required for the config file. This is a basic version for ChillMain :
.. code-block:: yaml
# config/config_test.yml
imports:
- { resource: config.yml } #here we import a config.yml file, this is not required
framework:
test: ~
session:
storage_id: session.storage.filesystem
.. code-block:: yaml
# config/config.yml
framework:
secret: Not very secret
router: { resource: "%kernel.root_dir%/config/routing.yml" }
form: true
csrf_protection: true
session: ~
default_locale: fr
translator: { fallback: fr }
profiler: { only_exceptions: false }
templating: #required for assetic. Remove if not needed
engines: ['twig']
.. note::
You must adapt config.yml file according to your required bundle. Some options will be missing, other may be removed...
.. note::
If you would like to tests different environments, with differents configuration, you could create differents config_XXX.yml files.
routing.yml
------------
You should add there all routing information needed for your bundle.
.. code-block: yaml
chill_main_bundle:
resource: "@CLChillMainBundle/Resources/config/routing.yml"
That's it. Tests should pass.

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

@@ -1,68 +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".
Run tests
*********
In reason of the Chill architecture, test should be runnable from the bundle's directory and works correctly: this will allow continuous integration tools to run tests automatically.
From chill app
==============
This is the most convenient method for developer: run test for chill bundle from the main app.
.. code-block:: bash
# run into a container
docker-compose exec --user $(id -u) php bash
# execute all tests suites
bin/phpunit
# .. or execute a single test
bin/phpunit vendor/chill-project/chill-bundles/src/Bundle/ChillMainBundle/Tests/path/to/FileTest.php
You can also run tests in a single command:
.. code-block:: bash
docker-compose exec --user $(id -u) php bin/phpunit
Tests from a bundle (chill-bundles)
-----------------------------------
Those tests needs the whole symfony app to execute Application Tests (which test html page).
For ease, the app is cloned using a :code:`git submodule`, which clone the main app into :code:`tests/app`, and tests are bootstrapped to this app. The dependencies are also installed into `tests/app/vendor` to ensure compliance with relative path from this symfony application.
You may boostrap the tests fro the chill bundle this way:
.. code-block:: bash
# ensure to be located into the environement (provided by docker suits well)
docker-compose exec --user $(id -u) php bash
# go to chill subdirectory
cd vendor/chill-project/chill-bundles
# install submodule
git submodule init
git submodule update
# install composer and dependencies
curl -sS https://getcomposer.org/installer | php
# run tests
bin/phpunit
.. note::
If you are on a fresh install, you will need to migrate database schema.
The path to console tool must be adapted to the app. To load migration and add fixtures, one can execute the following commands:
.. code-block:: bash
tests/app/bin/console doctrine:migrations:migrate
tests/app/bin/console doctrine:fixtures:load

View File

@@ -97,7 +97,7 @@ The has the following signature :
*
* @param string $context
* @param mixed[] $args the argument to the context.
* @return TimelineSingleQuery
* @return string[]
* @throw \LogicException if the context is not supported
*/
public function fetchQuery($context, array $args);
@@ -163,16 +163,18 @@ The has the following signature :
The `fetchQuery` function
^^^^^^^^^^^^^^^^^^^^^^^^^
The fetchQuery function help to build the UNION query to gather events. This function should return an instance of :code:`TimelineSingleQuery`. For you convenience, this object may be build using an associative array with the following keys:
The fetchQuery function help to build the UNION query to gather events. This function should return an associative array MUST have the following key :
* `id` : the name of the id column
* `type`: a string to indicate the type
* `date`: the name of the datetime column, used to order entities by date
* `FROM`: the FROM clause. May contains JOIN instructions
* `WHERE`: the WHERE clause;
* `parameters`: the parameters to pass to the query
* `FROM` (in capital) : the FROM clause. May contains JOIN instructions
The parameters should be replaced into the query by :code:`?`. They will be replaced into the query using prepared statements.
Those key are optional:
* `WHERE` (in capital) : the WHERE clause.
Where relevant, the data must be quoted to avoid SQL injection.
`$context` and `$args` are defined by the bundle which will call the timeline rendering. You may use them to build a different query depending on this context.
@@ -184,15 +186,6 @@ For instance, if the context is `'person'`, the args will be this array :
'person' => $person //a \Chill\PersonBundle\Entity\Person entity
);
For the context :code:`center`, the args will be:
.. code-block:: php
array(
'centers' => [ ] // an array of \Chill\MainBundle\Entity\Center entities
);
You should find in the bundle documentation which contexts are arguments the bundle defines.
.. note::
@@ -206,12 +199,13 @@ Example of an implementation :
namespace Chill\ReportBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Chill\MainBundle\Timeline\TimelineSingleQuery;
use Doctrine\ORM\EntityManager;
/**
* Provide report for inclusion in timeline
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @author Champs Libres <info@champs-libres.coop>
*/
class TimelineReportProvider implements TimelineProviderInterface
{
@@ -233,17 +227,16 @@ Example of an implementation :
$metadata = $this->em->getClassMetadata('ChillReportBundle:Report');
return TimelineSingleQuery::fromArray([
return array(
'id' => $metadata->getColumnName('id'),
'type' => 'report',
'date' => $metadata->getColumnName('date'),
'FROM' => $metadata->getTableName(),
'WHERE' => sprintf('%s = ?',
'WHERE' => sprintf('%s = %d',
$metadata
->getAssociationMapping('person')['joinColumns'][0]['name'])
)
'parameters' => [ $args['person']->getId() ]
]);
->getAssociationMapping('person')['joinColumns'][0]['name'],
$args['person']->getId())
);
}
//....

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

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\ComposerJsonManipulator\ValueObject\ComposerJsonSection;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushNextDevReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushTagReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetCurrentMutualDependenciesReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetNextMutualDependenciesReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\TagVersionReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateBranchAliasReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateReplaceReleaseWorker;
use Symplify\MonorepoBuilder\ValueObject\Option;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
// where are the packages located?
$parameters->set(Option::PACKAGE_DIRECTORIES, [
// default value
__DIR__ . '/src/Bundle',
]);
// for "merge" command
$parameters->set(
Option::DATA_TO_APPEND,
[
ComposerJsonSection::REQUIRE_DEV => [
'phpunit/phpunit' => '^9.5',
],
]
);
$services = $containerConfigurator->services();
# release workers - in order to execute
$services->set(UpdateReplaceReleaseWorker::class);
$services->set(SetCurrentMutualDependenciesReleaseWorker::class);
$services->set(TagVersionReleaseWorker::class);
$services->set(PushTagReleaseWorker::class);
$services->set(SetNextMutualDependenciesReleaseWorker::class);
$services->set(UpdateBranchAliasReleaseWorker::class);
$services->set(PushNextDevReleaseWorker::class);
};

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/app/tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<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>
<!-- test for export will be runned later -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Export/*</exclude>
<!-- we are rewriting accompanying periods... Work in progress -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingPeriodControllerTest.php</exclude>
<!-- we are rewriting address, Work in progress -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonAddressControllerTest.php</exclude>
<!-- find a solution to create multiple configs -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php</exclude>
<!-- temporarily removed, the time to find a fix -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
</testsuite>
</testsuites>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<!-- Run `composer require symfony/panther` before enabling this extension -->
<!--
<extensions>
<extension class="Symfony\Component\Panther\ServerExtension" />
</extensions>
-->
</phpunit>

View File

@@ -52,6 +52,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
$loader->load('services/export.yaml');
$loader->load('services/repositories.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/menu.yaml');
$loader->load('services/controller.yaml');
$loader->load('services/form.yaml');
$loader->load('services/templating.yaml');

View File

@@ -38,7 +38,7 @@ use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency;
* Class Activity
*
* @package Chill\ActivityBundle\Entity
* @ORM\Entity(repositoryClass="Chill\ActivityBundle\Repository\ActivityRepository")
* @ORM\Entity()
* @ORM\Table(name="activity")
* @ORM\HasLifecycleCallbacks()
* @UserCircleConsistency(

View File

@@ -1,169 +0,0 @@
<?php
/*
* Chill is a software for social workers
*
* Copyright (C) 2021, 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\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\Person;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Scope;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query\Expr\Orx;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\EntityManagerInterface;
final class ActivityACLAwareRepository
{
private AuthorizationHelper $authorizationHelper;
private TokenStorageInterface $tokenStorage;
private ActivityRepository $repository;
private EntityManagerInterface $em;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
ActivityRepository $repository,
EntityManagerInterface $em
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->repository = $repository;
$this->em = $em;
}
public function queryTimelineIndexer(string $context, array $args = []): array
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$from = $this->getFromClauseCenter($args);
[$where, $parameters] = $this->getWhereClause($context, $args);
return [
'id' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('id'),
'type' => 'activity',
'date' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('date'),
'FROM' => $from,
'WHERE' => $where,
'parameters' => $parameters
];
}
private function getFromClauseCenter(array $args): string
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
return $metadataActivity->getTableName().' JOIN '
.$metadataPerson->getTableName().' ON '
.$metadataPerson->getTableName().'.'.
$associationMapping['joinColumns'][0]['referencedColumnName']
.' = '
.$associationMapping['joinColumns'][0]['name']
;
}
private function getWhereClause(string $context, array $args): array
{
$where = '';
$parameters = [];
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$activityToPerson = $metadataActivity->getAssociationMapping('person')['joinColumns'][0]['name'];
$activityToScope = $metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name'];
$personToCenter = $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'];
// acls:
$role = new Role(ActivityVoter::SEE);
$reachableCenters = $this->authorizationHelper->getReachableCenters($this->tokenStorage->getToken()->getUser(),
$role);
if (count($reachableCenters) === 0) {
// insert a dummy condition
return 'FALSE = TRUE';
}
if ($context === 'person') {
// we start with activities having the person_id linked to person
$where .= sprintf('%s = ? AND ', $activityToPerson);
$parameters[] = $person->getId();
}
// we add acl (reachable center and scopes)
$where .= '('; // first loop for the for centers
$centersI = 0; // like centers#i
foreach ($reachableCenters as $center) {
// we pass if not in centers
if (!\in_array($center, $args['centers'])) {
continue;
}
// we get all the reachable scopes for this center
$reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), $role, $center);
// we get the ids for those scopes
$reachablesScopesId = array_map(
function(Scope $scope) { return $scope->getId(); },
$reachableScopes
);
// if not the first center
if ($centersI > 0) {
$where .= ') OR (';
}
// condition for the center
$where .= sprintf(' %s.%s = ? ', $metadataPerson->getTableName(), $personToCenter);
$parameters[] = $center->getId();
// begin loop for scopes
$where .= ' AND (';
$scopesI = 0; //like scope#i
foreach ($reachablesScopesId as $scopeId) {
if ($scopesI > 0) {
$where .= ' OR ';
}
$where .= sprintf(' %s.%s = ? ', $metadataActivity->getTableName(), $activityToScope);
$parameters[] = $scopeId;
$scopesI ++;
}
// close loop for scopes
$where .= ') ';
$centersI++;
}
// close loop for centers
$where .= ')';
return [$where, $parameters];
}
}

View File

@@ -1,42 +0,0 @@
<?php
/*
* Chill is a software for social workers
*
* Copyright (C) 2021, 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\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method AccompanyingPeriodParticipation|null find($id, $lockMode = null, $lockVersion = null)
* @method AccompanyingPeriodParticipation|null findOneBy(array $criteria, array $orderBy = null)
* @method AccompanyingPeriodParticipation[] findAll()
* @method AccompanyingPeriodParticipation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ActivityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Activity::class);
}
}

View File

@@ -1,11 +1,11 @@
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
<div>
<h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span>{% if 'person' != context %} / {{ activity.person|chill_entity_render_box({'addLink': true}) }}{% endif %}</h3>
<h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span></h3>
<div class="statement">
<span class="statement">{{ '%user% has done an %activity_type%'|trans(
{
'%user%' : activity.user,
'%user%' : user,
'%activity_type%': activity.type.name|localize_translatable_string,
'%date%' : activity.date|format_date('long') }
) }}</span>
@@ -29,13 +29,13 @@
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_show', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-view">
<a href="{{ path('chill_activity_activity_show', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-view">
{{ 'Show the activity'|trans }}
</a>
</li>
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_edit', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-edit">
<a href="{{ path('chill_activity_activity_edit', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-edit">
{{ 'Edit the activity'|trans }}
</a>
</li>

View File

@@ -21,7 +21,6 @@
namespace Chill\ActivityBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
use Doctrine\ORM\EntityManager;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -29,13 +28,13 @@ use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\Mapping\ClassMetadata;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Scope;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Timeline\TimelineSingleQuery;
/**
* Provide activity for inclusion in timeline
*
*/
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @author Champs Libres <info@champs-libres.coop>
*/
class TimelineActivityProvider implements TimelineProviderInterface
{
@@ -57,10 +56,6 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
protected $user;
protected ActivityACLAwareRepository $aclAwareRepository;
private const SUPPORTED_CONTEXTS = [ 'center', 'person'];
/**
* TimelineActivityProvider constructor.
*
@@ -71,13 +66,11 @@ class TimelineActivityProvider implements TimelineProviderInterface
public function __construct(
EntityManager $em,
AuthorizationHelper $helper,
TokenStorageInterface $storage,
ActivityACLAwareRepository $aclAwareRepository
TokenStorageInterface $storage
)
{
$this->em = $em;
$this->helper = $helper;
$this->aclAwareRepository = $aclAwareRepository;
if (!$storage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User)
{
@@ -93,69 +86,67 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
public function fetchQuery($context, array $args)
{
if ('center' === $context) {
return TimelineSingleQuery::fromArray($this->aclAwareRepository
->queryTimelineIndexer($context, $args));
}
$this->checkContext($context);
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity');
$metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person');
[$where, $parameters] = $this->getWhereClauseForPerson($args['person']);
return TimelineSingleQuery::fromArray([
return array(
'id' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('id'),
'type' => 'activity',
'date' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('date'),
'FROM' => $this->getFromClausePerson($args['person']),
'WHERE' => $where,
'parameters' => $parameters
]);
'FROM' => $this->getFromClause($metadataActivity, $metadataPerson),
'WHERE' => $this->getWhereClause($metadataActivity, $metadataPerson,
$args['person'])
);
}
private function getWhereClauseForPerson(Person $person)
private function getWhereClause(ClassMetadata $metadataActivity,
ClassMetadata $metadataPerson, Person $person)
{
$parameters = [];
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
$role = new Role('CHILL_ACTIVITY_SEE');
$reachableScopes = $this->helper->getReachableScopes($this->user,
$role, $person->getCenter());
$whereClause = sprintf(' {activity.person_id} = ? AND {activity.scope_id} IN ({scopes_ids}) ');
$scopes_ids = [];
$reachableCenters = $this->helper->getReachableCenters($this->user,
$role);
$associationMapping = $metadataActivity->getAssociationMapping('person');
// first parameter: activity.person_id
$parameters[] = $person->getId();
// loop on reachable scopes
foreach ($reachableScopes as $scope) {
if (\in_array($scope->getId(), $scopes_ids)) {
continue;
}
$scopes_ids[] = '?';
$parameters[] = $scope->getId();
if (count($reachableCenters) === 0) {
return 'FALSE = TRUE';
}
return [
\strtr(
$whereClause,
[
'{activity.person_id}' => $associationMapping['joinColumns'][0]['name'],
'{activity.scope_id}' => $metadataActivity->getTableName().'.'.
// we start with activities having the person_id linked to person
// (currently only context "person" is supported)
$whereClause = sprintf('%s = %d',
$associationMapping['joinColumns'][0]['name'],
$person->getId());
// we add acl (reachable center and scopes)
$centerAndScopeLines = array();
foreach ($reachableCenters as $center) {
$reachablesScopesId = array_map(
function(Scope $scope) { return $scope->getId(); },
$this->helper->getReachableScopes($this->user, $role,
$person->getCenter())
);
$centerAndScopeLines[] = sprintf('(%s = %d AND %s IN (%s))',
$metadataPerson->getTableName().'.'.
$metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'],
$center->getId(),
$metadataActivity->getTableName().'.'.
$metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name'],
'{scopes_ids}' => \implode(", ", $scopes_ids)
,
]
),
$parameters
];
implode(',', $reachablesScopesId));
}
$whereClause .= ' AND ('.implode(' OR ', $centerAndScopeLines).')';
return $whereClause;
}
private function getFromClausePerson()
private function getFromClause(ClassMetadata $metadataActivity,
ClassMetadata $metadataPerson)
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
return $metadataActivity->getTableName().' JOIN '
@@ -173,7 +164,7 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
public function getEntities(array $ids)
{
$activities = $this->em->getRepository(Activity::class)
$activities = $this->em->getRepository('ChillActivityBundle:Activity')
->findBy(array('id' => $ids));
$result = array();
@@ -192,13 +183,14 @@ class TimelineActivityProvider implements TimelineProviderInterface
{
$this->checkContext($context);
return [
return array(
'template' => 'ChillActivityBundle:Timeline:activity_person_context.html.twig',
'template_data' => [
'template_data' => array(
'activity' => $entity,
'context' => $context
]
];
'person' => $args['person'],
'user' => $entity->getUser()
)
);
}
/**
@@ -218,7 +210,7 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
private function checkContext($context)
{
if (FALSE === \in_array($context, self::SUPPORTED_CONTEXTS)) {
if ($context !== 'person') {
throw new \LogicException("The context '$context' is not "
. "supported. Currently only 'person' is supported");
}

View File

@@ -3,17 +3,13 @@
"description": "This bundle extend chill for recording the different activities of the user",
"type": "symfony-bundle",
"license": "AGPL-3.0",
"keywords": ["chill", "social work"],
"homepage": "https://github.com/Chill-project/Activity",
"keywords" : ["chill", "social work"],
"homepage" : "https://github.com/Chill-project/Activity",
"autoload": {
"psr-4": {
"Chill\\ActivityBundle\\": ""
}
"psr-4": { "Chill\\ActivityBundle\\": "" }
},
"autoload-dev": {
"classmap": [
"Resources/test/Fixtures/App/app/AppKernel.php"
]
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
},
"authors": [
{
@@ -21,6 +17,10 @@
"email": "info@champs-libres.coop"
}
],
"require": {
},
"require-dev": {
},
"scripts": {
"post-install-cmd": [
"ComposerBundleMigration\\Composer\\Migrations::synchronizeMigrations",

View File

@@ -22,14 +22,6 @@ services:
- '@doctrine.orm.entity_manager'
- '@chill.main.security.authorization.helper'
- '@security.token_storage'
- '@Chill\ActivityBundle\Repository\ActivityACLAwareRepository'
public: true
tags:
- { name: chill.timeline, context: 'person' }
- { name: chill.timeline, context: 'center' }
Chill\ActivityBundle\Menu\:
autowire: true
autoconfigure: true
resource: '../Menu/'
tags: ['chill.menu_builder']

View File

@@ -0,0 +1,8 @@
services:
Chill\ActivityBundle\Menu\MenuBuilder:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }

View File

@@ -1,4 +1,3 @@
---
services:
chill_activity.repository.activity_type:
class: Doctrine\ORM\EntityRepository
@@ -17,16 +16,3 @@ services:
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments:
- 'Chill\ActivityBundle\Entity\ActivityReasonCategory'
Chill\ActivityBundle\Repository\ActivityRepository:
tags: [doctrine.repository_service]
arguments:
- '@Doctrine\Persistence\ManagerRegistry'
Chill\ActivityBundle\Repository\ActivityACLAwareRepository:
arguments:
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$repository: '@Chill\ActivityBundle\Repository\ActivityRepository'
$em: '@Doctrine\ORM\EntityManagerInterface'

View File

@@ -3,12 +3,13 @@
"description": "This bundle extend chill for recording element of a budget for peoples",
"type": "symfony-bundle",
"license": "AGPL-3.0",
"keywords": ["chill", "social work"],
"homepage": "https://framagit.org/Chill-project/BudgetBundle",
"keywords" : ["chill", "social work"],
"homepage" : "https://framagit.org/Chill-project/BudgetBundle",
"autoload": {
"psr-4": {
"Chill\\AMLI\\BudgetBundle\\": ""
}
"psr-4": { "Chill\\AMLI\\BudgetBundle\\": "" }
},
"autoload-dev": {
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
},
"authors": [
{
@@ -16,8 +17,13 @@
"email": "info@champs-libres.coop"
}
],
"require": {
},
"require-dev": {
},
"extra": {
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations"
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations",
"symfony-app-dir": "Test/Fixtures/App/app/"
},
"minimum-stability": "dev",
"prefer-stable": true

View File

@@ -3,25 +3,25 @@
"license": "AGPL-3.0",
"type": "symfony-bundle",
"description": "This bundle allow to add custom fields on entities.",
"keywords": ["chill", "social work"],
"homepage": "https://github.com/Chill-project/CustomFields",
"keywords" : ["chill", "social work"],
"homepage" : "https://github.com/Chill-project/CustomFields",
"autoload": {
"psr-4": {
"Chill\\CustomFieldsBundle\\": ""
}
"psr-4": { "Chill\\CustomFieldsBundle\\": "" }
},
"autoload-dev": {
"classmap": [
"Resources/test/Fixtures/App/app/AppKernel.php"
]
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
},
"authors": [
"authors" : [
{
"name": "Champs-Libres",
"email": "info@champs-libres.coop",
"homepage": "http://www.champs-libres.coop"
}
],
"require": {
},
"require-dev": {
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {

View File

@@ -3,9 +3,9 @@
"description": "A Chill bundle to store documents",
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Chill\\DocStoreBundle\\": ""
}
"psr-4": { "Chill\\DocStoreBundle\\" : "" }
},
"require": {
},
"license": "AGPL-3.0"
}

View File

@@ -23,7 +23,6 @@ namespace Chill\EventBundle\Timeline;
use Chill\EventBundle\Entity\Event;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Chill\MainBundle\Timeline\TimelineSingleQuery;
use Doctrine\ORM\EntityManager;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -89,14 +88,13 @@ class TimelineEventProvider implements TimelineProviderInterface
$metadataParticipation = $this->em->getClassMetadata('ChillEventBundle:Participation');
$metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person');
$query = TimelineSingleQuery::fromArray([
$query = array(
'id' => $metadataEvent->getTableName().'.'.$metadataEvent->getColumnName('id'),
'type' => 'event',
'date' => $metadataEvent->getTableName().'.'.$metadataEvent->getColumnName('date'),
'FROM' => $this->getFromClause($metadataEvent, $metadataParticipation, $metadataPerson),
'WHERE' => $this->getWhereClause($metadataEvent, $metadataParticipation, $metadataPerson, $args['person']),
'parameters' => []
]);
'WHERE' => $this->getWhereClause($metadataEvent, $metadataParticipation, $metadataPerson, $args['person'])
);
return $query;
}

View File

@@ -3,18 +3,16 @@
"description": "This bundle extend chill software. This bundle allow to define event and participation to those events.",
"type": "symfony-bundle",
"license": "AGPL-3.0-only",
"keywords": ["chill", "social work"],
"homepage": "https://git.framasoft.org/Chill-project/Chill-Group",
"keywords" : ["chill", "social work"],
"homepage" : "https://git.framasoft.org/Chill-project/Chill-Group",
"autoload": {
"psr-4": {
"Chill\\EventBundle\\": ""
}
"psr-4": { "Chill\\EventBundle\\": "" }
},
"support": {
"issues": "https://git.framasoft.org/Chill-project/Chill-Event/issues",
"source": "https://git.framasoft.org/Chill-project/Chill-Event",
"docs": "http://docs.chill.social",
"email": "dev@listes.chill.social"
"docs" : "http://docs.chill.social",
"email": "dev@listes.chill.social"
},
"authors": [
{
@@ -22,7 +20,11 @@
"email": "info@champs-libres.coop"
}
],
"suggest": {
"require": {
},
"require-dev": {
},
"suggest" : {
"chill-project/group": "dev-master@dev"
},
"scripts": {

View File

@@ -3,12 +3,13 @@
"description": "This bundle extend chill for recording family members into a file",
"type": "symfony-bundle",
"license": "AGPL-3.0-or-later",
"keywords": ["chill", "social work"],
"homepage": "https://framagit.org/Chill-project/Chill-FamilyMembers",
"keywords" : ["chill", "social work"],
"homepage" : "https://framagit.org/Chill-project/Chill-FamilyMembers",
"autoload": {
"psr-4": {
"Chill\\AMLI\\FamilyMembersBundle\\": ""
}
"psr-4": { "Chill\\AMLI\\FamilyMembersBundle\\": "" }
},
"autoload-dev": {
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
},
"authors": [
{
@@ -16,8 +17,13 @@
"email": "info@champs-libres.coop"
}
],
"require": {
},
"require-dev": {
},
"extra": {
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations"
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations",
"symfony-app-dir": "Test/Fixtures/App/app/"
},
"minimum-stability": "dev",
"prefer-stable": true

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,251 +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;
}
/**
* Create an entity.
*
* @param string $action
* @param Request $request
* @return object
*/
protected function createEntity(string $action, Request $request): object
{
$type = $this->getEntityClass();
return new $type;
}
/**
* 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,517 +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);
case Request::METHOD_POST:
return $this->entityPostAction('_entity', $request, $id, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
}
}
public function entityPost(Request $request, $_format): Response
{
switch($request->getMethod()) {
case Request::METHOD_POST:
return $this->entityPostAction('_entity', $request, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
}
}
protected function entityPostAction($action, Request $request, string $_format): Response
{
$entity = $this->createEntity($action, $request);
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;
}
$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;
}
$this->getDoctrine()->getManager()->persist($entity);
$this->getDoctrine()->getManager()->flush();
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
if ($response instanceof Response) {
return $response;
}
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
return $this->json(
$entity,
Response::HTTP_OK,
[],
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity)
);
}
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:
case Request::METHOD_POST:
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');
}
}

View File

@@ -34,7 +34,6 @@ use Chill\MainBundle\CRUD\Form\CRUDDeleteEntityForm;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Class CRUDController
@@ -485,7 +484,7 @@ class CRUDController extends AbstractController
* @param mixed $id
* @return Response
*/
protected function viewAction(string $action, Request $request, $id, $_format = 'html'): Response
protected function viewAction(string $action, Request $request, $id)
{
$entity = $this->getEntity($action, $id, $request);
@@ -497,7 +496,7 @@ class CRUDController extends AbstractController
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
. "is not found"), $this->getCrudName(), $id);
}
$response = $this->checkACL($action, $entity);
@@ -510,36 +509,17 @@ class CRUDController extends AbstractController
return $response;
}
if ($_format === 'html') {
$defaultTemplateParameters = [
'entity' => $entity,
'crud_name' => $this->getCrudName()
];
$defaultTemplateParameters = [
'entity' => $entity,
'crud_name' => $this->getCrudName()
];
return $this->render(
$this->getTemplateFor($action, $entity, $request),
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
return $this->render(
$this->getTemplateFor($action, $entity, $request),
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
);
} elseif ($_format === 'json') {
$context = $this->getContextForSerialization($action, $request, $entity, $_format);
return $this->json($entity, Response::HTTP_OK, [], $context);
} else {
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented");
}
}
/**
* Get the context for the serialization
*/
public function getContextForSerialization(string $action, Request $request, $entity, string $_format): array
{
return [];
}
/**
* The edit action.
*
@@ -590,7 +570,7 @@ class CRUDController extends AbstractController
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
. "is not found"), $this->getCrudName(), $id);
}
$response = $this->checkACL($action, $entity);
@@ -819,7 +799,7 @@ class CRUDController extends AbstractController
*/
protected function getRoleFor($action)
{
if (\array_key_exists('role', $this->getActionConfig($action))) {
if (NULL !== ($this->getActionConfig($action)['role'])) {
return $this->getActionConfig($action)['role'];
}
@@ -1201,7 +1181,6 @@ class CRUDController extends AbstractController
AuthorizationHelper::class => AuthorizationHelper::class,
EventDispatcherInterface::class => EventDispatcherInterface::class,
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
]
);
}

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,153 +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->apiConfig as $crudConfig) {
$collection->addCollection($this->loadApi($crudConfig));
foreach ($this->config as $config) {
$collection->addCollection($this->loadConfig($config));
}
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));
if (count($methods) === 0) {
throw new \RuntimeException("The api configuration named \"{$crudConfig['name']}\", action \"{$name}\", ".
"does not have any allowed methods. You should remove this action from the config ".
"or allow, at least, one method");
}
if ('_entity' === $name && \in_array(Request::METHOD_POST, $methods)) {
unset($methods[\array_search(Request::METHOD_POST, $methods)]);
$entityPostRoute = $this->createEntityPostRoute($name, $crudConfig, $action,
$controller);
$collection->add("chill_api_single_{$crudConfig['name']}_{$name}_create",
$entityPostRoute);
}
if (count($methods) === 0) {
// the only method was POST,
// continue to next
continue;
}
$route = new Route($path, $defaults, $requirements);
$route->setMethods($methods);
$collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route);
}
return $collection;
}
private function createEntityPostRoute(string $name, $crudConfig, array $action, $controller): Route
{
$localPath = $action['path'].'.{_format}';
$defaults = [
'_controller' => $controller.':'.($action['controller_action'] ?? 'entityPost')
];
$path = $crudConfig['base_path'].$localPath;
$requirements = $action['requirements'] ?? [];
$route = new Route($path, $defaults, $requirements);
$route->setMethods([ Request::METHOD_POST ]);
return $route;
}
}

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,27 +0,0 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
/**
* Class AddressReferenceAPIController
*
* @package Chill\MainBundle\Controller
* @author Champs Libres
*/
class AddressReferenceAPIController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('postal_code')) {
$qb->where('e.postcode = :postal_code')
->setParameter('postal_code', $request->query->get('postal_code'));
}
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
/**
* Class PostalCodeAPIController
*
* @package Chill\MainBundle\Controller
* @author Champs Libres
*/
class PostalCodeAPIController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('country')) {
$qb->where('e.country = :country')
->setParameter('country', $request->query->get('country'));
}
}
}

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;
}
@@ -147,19 +153,6 @@ class SearchController extends AbstractController
);
}
public function searchApi(Request $request, $_format): JsonResponse
{
//TODO this is an incomplete implementation
$query = $request->query->get('q', '');
$results = $this->searchApi->getResults($query, 0, 150);
$paginator = $this->paginatorFactory->create(count($results));
$collection = new Collection($results, $paginator);
return $this->json($collection);
}
public function advancedSearchListAction(Request $request)
{
/* @var $variable Chill\MainBundle\Search\SearchProvider */

View File

@@ -1,91 +0,0 @@
<?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\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Timeline\TimelineBuilder;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
class TimelineCenterController extends AbstractController
{
protected TimelineBuilder $timelineBuilder;
protected PaginatorFactory $paginatorFactory;
private Security $security;
public function __construct(
TimelineBuilder $timelineBuilder,
PaginatorFactory $paginatorFactory,
Security $security
) {
$this->timelineBuilder = $timelineBuilder;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security;
}
/**
* @Route("/{_locale}/center/timeline",
* name="chill_center_timeline",
* methods={"GET"}
* )
*/
public function centerAction(Request $request)
{
// collect reachable center for each group
$user = $this->security->getUser();
$centers = [];
foreach ($user->getGroupCenters() as $group) {
$centers[] = $group->getCenter();
}
if (0 === count($centers)) {
throw $this->createNotFoundException();
}
$nbItems = $this->timelineBuilder->countItems('center',
[ 'centers' => $centers ]
);
$paginator = $this->paginatorFactory->create($nbItems);
return $this->render('@ChillMain/Timeline/index.html.twig', array
(
'timeline' => $this->timelineBuilder->getTimelineHTML(
'center',
[ 'centers' => $centers ],
$paginator->getCurrentPage()->getFirstItemNumber(),
$paginator->getItemsPerPage()
),
'nb_items' => $nbItems,
'paginator' => $paginator
)
);
}
}

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,9 +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 Chill\MainBundle\Doctrine\Type\NativeDateIntervalType;
use Chill\MainBundle\Doctrine\Type\PointType;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ChillMainExtension
@@ -134,9 +131,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/templating.yaml');
$loader->load('services/timeline.yaml');
$loader->load('services/search.yaml');
$loader->load('services/serializer.yaml');
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
$this->configureCruds($container, $config['cruds'], $loader);
}
/**
@@ -169,49 +165,32 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->prependExtensionConfig('twig', $twigConfig);
//add DQL function to ORM (default entity_manager)
$container
->prependExtensionConfig(
'doctrine',
[
'orm' => [
'dql' => [
'string_functions' => [
'unaccent' => Unaccent::class,
'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class,
'AGGREGATE' => JsonAggregate::class,
'REPLACE' => Replace::class,
],
'numeric_functions' => [
'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class,
'SIMILARITY' => Similarity::class,
'OVERLAPSI' => OverlapsI::class,
],
],
],
],
);
$container->prependExtensionConfig('doctrine', array(
'orm' => array(
'dql' => array(
'string_functions' => array(
'unaccent' => Unaccent::class,
'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class,
'AGGREGATE' => JsonAggregate::class,
'REPLACE' => Replace::class,
),
'numeric_functions' => [
'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class,
'SIMILARITY' => Similarity::class,
'OVERLAPSI' => OverlapsI::class
]
)
)
));
//add dbal types (default entity_manager)
$container
->prependExtensionConfig(
'doctrine',
[
'dbal' => [
// This is mandatory since we are using postgis as database.
'mapping_types' => [
'geometry' => 'string',
],
'types' => [
'dateinterval' => [
'class' => NativeDateIntervalType::class
],
'point' => [
'class' => PointType::class
]
]
]
]
);
$container->prependExtensionConfig('doctrine', array(
'dbal' => [
'types' => [
'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class
]
]
));
//add current route to chill main
$container->prependExtensionConfig('chill_main', array(
@@ -227,125 +206,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
]
],
]
],
[
'controller' => \Chill\MainBundle\Controller\AddressReferenceAPIController::class,
'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
]
],
]
],
[
'controller' => \Chill\MainBundle\Controller\PostalCodeAPIController::class,
'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 = null;
private ?float $lon = null;
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,9 +4,6 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Chill\MainBundle\Doctrine\Model\Point;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* Address
@@ -23,7 +20,6 @@ class Address
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"write"})
*/
private $id;
@@ -31,82 +27,23 @@ class Address
* @var string
*
* @ORM\Column(type="string", length=255)
* @groups({"write"})
*/
private $street = '';
private $streetAddress1 = '';
/**
* @var string
*
* @ORM\Column(type="string", length=255)
* @groups({"write"})
*/
private $streetNumber = '';
private $streetAddress2 = '';
/**
* @var PostalCode
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"write"})
*/
private $postcode;
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/
private $floor;
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/
private $corridor;
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/
private $steps;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/
private $buildingName;
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/
private $flat;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/
private $distribution;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/
private $extra;
/**
* Indicates when the address starts validation. Used to build an history
* of address. By default, the current date.
@@ -114,50 +51,16 @@ class Address
* @var \DateTime
*
* @ORM\Column(type="date")
* @groups({"write"})
*/
private $validFrom;
/**
* Indicates when the address ends. Used to build an history
* of address.
*
* @var \DateTime|null
*
* @ORM\Column(type="date", nullable=true)
* @groups({"write"})
*/
private $validTo;
/**
* True if the address is a "no address", aka homeless person, ...
* @groups({"write"})
*
* @var bool
*/
private $isNoAddress = false;
/**
* A geospatial field storing the coordinates of the Address
*
* @var Point|null
*
* @ORM\Column(type="point", nullable=true)
* @groups({"write"})
*/
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")
* @groups({"write"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $linkedToThirdParty;
/**
* A list of metadata, added by customizable fields
*
@@ -170,6 +73,7 @@ class Address
$this->validFrom = new \DateTime();
}
/**
* Get id
*
@@ -181,7 +85,7 @@ class Address
}
/**
* Set streetAddress1 (legacy function)
* Set streetAddress1
*
* @param string $streetAddress1
*
@@ -189,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
*
@@ -213,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;
}
/**
@@ -382,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,173 +0,0 @@
<?php
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Doctrine\Model\Point;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Entity()
* @ORM\Table(name="chill_main_address_reference")
* @ORM\HasLifecycleCallbacks()
*/
class AddressReference
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @groups({"read"})
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @groups({"read"})
*/
private $refId;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $street;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $streetNumber;
/**
* @var PostalCode
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"read"})
*/
private $postcode;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $municipalityCode;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $source;
/**
* A geospatial field storing the coordinates of the Address
*
* @var Point
*
* @ORM\Column(type="point")
* @groups({"read"})
*/
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

@@ -3,7 +3,6 @@
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* Country
@@ -21,7 +20,6 @@ class Country
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
*/
private $id;
@@ -29,8 +27,6 @@ class Country
* @var string
*
* @ORM\Column(type="json_array")
* @groups({"read"})
*
*/
private $name;
@@ -38,7 +34,6 @@ class Country
* @var string
*
* @ORM\Column(type="string", length=3)
* @groups({"read"})
*/
private $countryCode;

View File

@@ -3,7 +3,6 @@
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* PostalCode
@@ -26,7 +25,6 @@ class PostalCode
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
*/
private $id;
@@ -34,7 +32,6 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=255, name="label")
* @groups({"read"})
*/
private $name;
@@ -42,7 +39,6 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=100)
* @groups({"read"})
*/
private $code;
@@ -50,7 +46,6 @@ class PostalCode
* @var Country
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
* @groups({"read"})
*/
private $country;

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

@@ -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(

View File

@@ -27,7 +27,6 @@ use Symfony\Component\Form\FormBuilderInterface;
use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer;
use Doctrine\Persistence\ObjectManager;
use Chill\MainBundle\Form\Type\Select2ChoiceType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
/**
* Extends choice to allow adding select2 library on widget
@@ -42,26 +41,15 @@ class Select2CountryType extends AbstractType
*/
private $requestStack;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
/**
* @var ObjectManager
*/
private $em;
public function __construct(
RequestStack $requestStack,
ObjectManager $em,
TranslatableStringHelper $translatableStringHelper
)
public function __construct(RequestStack $requestStack,ObjectManager $em)
{
$this->requestStack = $requestStack;
$this->em = $em;
$this->translatableStringHelper = $translatableStringHelper;
}
public function getBlockPrefix()
@@ -87,7 +75,7 @@ class Select2CountryType extends AbstractType
$choices = array();
foreach ($countries as $c) {
$choices[$c->getId()] = $this->translatableStringHelper->localize($c->getName());
$choices[$c->getId()] = $c->getName()[$locale];
}
asort($choices, SORT_STRING | SORT_FLAG_CASE);

View File

@@ -27,7 +27,6 @@ use Symfony\Component\Form\FormBuilderInterface;
use Chill\MainBundle\Form\Type\DataTransformer\MultipleObjectsToIdTransformer;
use Doctrine\Persistence\ObjectManager;
use Chill\MainBundle\Form\Type\Select2ChoiceType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
/**
* Extends choice to allow adding select2 library on widget for languages (multiple)
@@ -44,21 +43,10 @@ class Select2LanguageType extends AbstractType
*/
private $em;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(
RequestStack $requestStack,
ObjectManager $em,
TranslatableStringHelper $translatableStringHelper
)
public function __construct(RequestStack $requestStack,ObjectManager $em)
{
$this->requestStack = $requestStack;
$this->em = $em;
$this->translatableStringHelper = $translatableStringHelper;
}
public function getBlockPrefix()
@@ -84,7 +72,7 @@ class Select2LanguageType extends AbstractType
$choices = array();
foreach ($languages as $l) {
$choices[$l->getId()] = $this->translatableStringHelper->localize($l->getName());
$choices[$l->getId()] = $l->getName()[$locale];
}
asort($choices, SORT_STRING | SORT_FLAG_CASE);

View File

@@ -104,7 +104,7 @@ class Mailer
* @param \User $to
* @param array $subject Subject of the message [ 0 => $message (required), 1 => $parameters (optional), 3 => $domain (optional) ]
* @param array $bodies The bodies. An array where keys are the contentType and values the bodies
* @param callable $callback a callback to customize the message (add attachment, etc.)
* @param \callable $callback a callback to customize the message (add attachment, etc.)
*/
public function sendNotification(
$recipient,

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

@@ -23,12 +23,12 @@
// @import "bootstrap/scss/button-group";
// @import "bootstrap/scss/input-group";
// @import "bootstrap/scss/custom-forms";
@import "bootstrap/scss/nav";
// @import "bootstrap/scss/nav";
// @import "bootstrap/scss/navbar";
// @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

@@ -10,11 +10,5 @@ div.chill_address {
text-indent: -1.5em;
}
&.chill_address_address--multiline {
p {
display: block;
}
}
}
}

View File

@@ -9,11 +9,6 @@ ul.record_actions li {
ul.record_actions, ul.record_actions_column {
display: flex;
justify-content: flex-end;
&.record_actions--left {
justify-content: flex-start;
}
padding: 0.5em 0;
flex-wrap: wrap-reverse;

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,
@@ -58,11 +57,6 @@
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

@@ -1,57 +1,21 @@
/*
* NOTE 2021.04
* scss/chillmain.scss is the main sass file for the new chill.2
* scss/chill.scss is the main sass file for the new chill.2
* scratch will be replaced by bootstrap, please avoid to edit in modules/scratch/_custom.scss
*
* when possible, try to use bootstrap html class
* when possible, try to use bootstrap class naming
*/
/* [hack] /!\ Contourne le positionnement problématique du div#content_conainter suivant,
* car sa position: relative le place au-dessus du bandeau et les liens sont incliquables */
div.subheader {
height: 130px;
}
/*
* Specific rules
*/
// [scratch] un bouton 'disabled' non clickable
.sc-button {
&.disabled {
cursor: default;
&.bt-remove {
background-color: #d9d9d9;
}
}
}
// [debug] un affichage discret pour le debug
.discret {
color: grey;
margin-right: 1em;
}
// reserre la hauteur des rangées de tableau (ul.record_actions prennait trop de place)
table {
ul.record_actions {
margin: 0;
padding: 0.5em;
}
}
/*
* ACCOMPANYING_COURSE
* Header custom for Accompanying Course
*/
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;
@@ -61,6 +25,7 @@ div#header-accompanying_course-name {
}
}
}
div#header-accompanying_course-details {
background: none repeat scroll 0 0 #718596ab;
color: #FFF;
@@ -68,196 +33,25 @@ div#header-accompanying_course-details {
padding-bottom: 1em;
}
/*
* FLEX RESPONSIVE TABLE/BLOCK PRESENTATION
*/
div.flex-bloc,
div.flex-table {
h2, h3, h4, dl, p {
margin: 0;
}
h2, h3, h4 {
color: var(--chill-blue);
}
/* /!\ Contourne le positionnement problématique du div#content_conainter suivant,
* car sa position: relative le place au-dessus du bandeau et les liens sont incliquables */
div.subheader {
height: 130px;
}
/*
* Bloc appearance
*/
div.flex-bloc {
box-sizing: border-box;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
align-content: stretch;
div.item-bloc {
flex-grow: 0; flex-shrink: 1; flex-basis: 50%;
margin: 0;
border: 1px solid #000;
padding: 1em;
border-top: 0;
&:nth-child(1), &:nth-child(2) {
border-top: 1px solid #000;
}
border-left: 0;
&:nth-child(odd) {
border-left: 1px solid #000;
}
//background-color: #e6e6e6;
display: flex;
flex-direction: column;
div.item-row {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
flex-direction: column;
div.item-col {
&:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: auto;
}
&:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
.list-content { // ul, dl, or div
}
ul.record_actions {
margin: 0;
align-self: flex-end;
flex-grow: 1; flex-shrink: 0; flex-basis: auto;
li {
margin-right: 5px;
}
}
}
}
}
}
@media only screen and (max-width: 945px) { margin: auto -0.2em; }
@media only screen and (max-width: 935px) { margin: auto -0.5em; }
@media only screen and (max-width: 920px) { margin: auto -0.9em; }
@media only screen and (max-width: 900px) {
flex-direction: column;
margin: auto 0;
div.item-bloc {
border-left: 1px solid #000;
&:nth-child(2) {
border-top: 0;
}
}
}
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; }
}
/*
* Table appearance
*/
div.flex-table {
display: flex;
flex-direction: column;
align-items: stretch;
align-content: stretch;
div.item-bloc {
display: flex;
flex-direction: column;
padding: 1em;
border: 1px solid #000;
border-top: 0;
&:first-child {
border-top: 1px solid #000;
}
&:nth-child(even) {
background-color: #e6e6e6;
}
div.item-row {
display: flex;
flex-direction: row;
&:not(:first-child) {
margin-top: 0.5em;
border-top: 1px dotted #0000004f;
padding-top: 0.5em;
flex-direction: column;
}
div.item-col {
&:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: 33%;
}
&:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
justify-content: flex-end;
.list-content { // ul, dl, or div
}
ul.record_actions {
margin: 0;
align-self: flex-start;
flex-grow: 1; flex-shrink: 0; flex-basis: auto;
li {
margin-right: 5px;
}
}
}
}
@media only screen and (max-width: 900px) {
flex-direction: column;
div.item-col {
&:last-child {
ul.record_actions {
align-self: flex-end;
}
}
}
}
// neutralize
div.chill_address div.chill_address_address p { text-indent: 0; }
}
}
}
/*
* Address form
*/
div.address_form {
display: flex;
flex-direction: column;
div.address_form__header {
}
div.address_form__select {
display: flex;
flex-direction: row;
justify-content: space-between;
div.address_form__select__body {
display: flex;
flex-direction: column;
flex-grow: 1;
}
div.address_form__select__map {
margin: 0px 20px;
div#address_map {
height:400px;
width:400px;
}
}
}
div.address_form__more {
}
}

View File

@@ -1,60 +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);
let newAddress = {
'isNoAddress': address.isNoAddress,
'street': address.selected.address.street,
'streetNumber': address.selected.address.streetNumber,
'postcode': {'id': address.selected.city.id },
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
'flat': address.flat,
'buildingName': address.buildingName,
'distribution': address.distribution,
'extra': address.extra
};
if (address.selected.address.point !== undefined){
newAddress = Object.assign(newAddress, {'point': address.selected.address.point.coordinates});
}
this.$store.dispatch('addAddress', newAddress);
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,23 +0,0 @@
const addressMessages = {
fr: {
add_an_address_title: 'Ajouter une adresse',
select_an_address_title: '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',
create_address: 'Appuyer sur "Entrée" pour créer une nouvelle adresse',
isNoAddress: 'Pas d\'adresse complète',
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,45 +0,0 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postAddress } from '../../_api/AddAddress'
const debug = process.env.NODE_ENV !== 'production';
const store = createStore({
strict: debug,
state: {
address: {},
errorMsg: []
},
getters: {
},
mutations: {
catchError(state, error) {
state.errorMsg.push(error);
},
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 la suite
//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)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
}
});
export { store };

View File

@@ -1,75 +0,0 @@
/*
* Endpoint chill_api_single_country__index
* method GET, get Country Object
* @returns {Promise} a promise containing all Country object
*/
const fetchCountries = () => {
console.log('<<< fetching countries');
const url = `/api/1.0/main/country.json?item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_postal_code__index
* method GET, get Country Object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/
const fetchCities = (country) => {
console.log('<<< fetching cities for', country);
const url = `/api/1.0/main/postal-code.json?item_per_page=1000&country=${country.id}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code
*/
const fetchReferenceAddresses = (postalCode) => {
console.log('<<< fetching references addresses for', postalCode);
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address-reference.json?item_per_page=1000&postal_code=${postalCode.id}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__entity__create
* method POST, post Address Object
* @returns {Promise}
*/
const postAddress = (address) => {
console.log(address);
const url = `/api/1.0/main/address.json?`;
const body = address;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
fetchCountries,
fetchCities,
fetchReferenceAddresses,
postAddress
};

View File

@@ -1,205 +0,0 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_an_address_title') }}
</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_title') }}</h3>
</template>
<template v-slot:body>
<div class="address_form">
<div class="address_form__header">
<h4>{{ $t('select_an_address_title') }}</h4>
</div>
<div class="address_form__select">
<div class="address_form__select__body">
<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>
</div>
<div class="address_form__select__map">
<address-map
v-bind:address="address"
ref="addressMap">
</address-map>
</div>
</div>
<div class="address_form__more">
<address-more
v-if="!isNoAddress"
v-bind:address="address">
</address-more>
</div>
</div>
</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;
}
}
},
mounted() {
this.getCountries();
},
methods: {
openModal() {
this.modal.showModal = true;
this.resetAll();
//this.$nextTick(function() {
// this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale
//})
},
getCountries() {
console.log('getCountries');
fetchCountries().then(countries => new Promise((resolve, reject) => {
this.address.loaded.countries = countries.results;
resolve()
}))
.catch((error) => {
this.errorMsg.push(error.message);
});
},
getCities(country) {
console.log('getCities for', country.name);
fetchCities(country).then(cities => new Promise((resolve, reject) => {
this.address.loaded.cities = cities.results;
resolve()
}))
.catch((error) => {
this.errorMsg.push(error.message);
});
},
getReferenceAddresses(city) {
console.log('getReferenceAddresses for', city.name);
fetchReferenceAddresses(city).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,50 +0,0 @@
<template>
<div class="container">
<div id='address_map'></div>
</div>
</template>
<script>
import L from 'leaflet';
import markerIconPng from 'leaflet/dist/images/marker-icon.png'
import 'leaflet/dist/leaflet.css';
let map;
let marker;
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,
iconAnchor: [12, 41],
});
marker = L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map);
},
update() {
console.log('update map with : ', this.address.addressMap.center)
marker.setLatLng(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,56 +0,0 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
@select="selectAddress"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:taggable="true"
:multiple="false"
@tag="addAddress"
:placeholder="$t('select_address')"
:tagPlaceholder="$t('create_address')"
:options="addresses">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'AddressSelection',
components: { VueMultiselect },
props: ['address', 'updateMapCenter'],
data() {
return {
value: null
}
},
computed: {
addresses() {
return this.address.loaded.addresses;
}
},
methods: {
transName(value) {
return value.streetNumber === undefined ? value.street : `${value.street}, ${value.streetNumber}`
},
selectAddress(value) {
this.address.selected.address = value;
this.updateMapCenter(value.point);
},
addAddress (newAddress) {
const address = {
street: newAddress
};
this.value = address;
this.address.selected.address = address;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,45 +0,0 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
@select="selectCity"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:options="cities">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'CitySelection',
components: { VueMultiselect },
props: ['address', 'getReferenceAddresses'],
data() {
return {
value: null
}
},
methods: {
transName(value) {
return `${value.code}-${value.name}`
},
selectCity(value) {
this.address.selected.city = value;
this.getReferenceAddresses(value);
},
},
computed: {
cities() {
return this.address.loaded.cities;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,59 +0,0 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
name="field"
track-by="id"
label="name"
:custom-label="transName"
:placeholder="$t('select_country')"
:options="countries"
@select="selectCountry">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'CountrySelection',
components: { VueMultiselect },
props: ['address', 'getCities'],
data() {
return {
value: this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
}
},
methods: {
init() {
if (this.value !== undefined) {
this.getCities(this.value);
}
},
transName ({ name }) {
return name.fr //TODO multilang
},
selectCountry(value) {
console.log(value);
this.address.selected.country = value;
this.getCities(value);
},
},
mounted(){
this.init()
},
computed: {
countries() {
const countries = this.address.loaded.countries;
let orderedCountries = [];
orderedCountries.push(...countries.filter(c => c.countryCode === 'FR'))
orderedCountries.push(...countries.filter(c => c.countryCode === 'BE'))
orderedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE'))
return orderedCountries;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

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,153 +0,0 @@
<template>
<a class="sc-button" target="_blank"
:class="classAction"
:title="$t(titleAction)"
@click="openModal">
{{ buttonText }}
</a>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t(titleModal) }}</h3>
</template>
<template v-slot:body v-if="type === 'person'">
<on-the-fly-person
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castPerson">
</on-the-fly-person>
</template>
<template v-slot:body v-else-if="type === 'thirdparty'">
<on-the-fly-thirdparty
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castThirdparty">
</on-the-fly-thirdparty>
</template>
<template v-slot:body v-else>
<on-the-fly-create
v-bind:action="action"
ref="castNew">
</on-the-fly-create>
</template>
<template v-slot:footer>
<button v-if="action === 'show'"
@click="changeActionTo('edit')"
class="sc-button bt-update"> <!-- @click.prevent="$emit('..', ..)" -->
</button>
<button v-else
class="sc-button bt-save"
@click="saveAction"
> <!--
-->
{{ $t('action.save')}}
</button>
</template>
</modal>
</teleport>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import OnTheFlyPerson from 'ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue';
import OnTheFlyThirdparty from './OnTheFly/ThirdParty.vue';
import OnTheFlyCreate from './OnTheFly/Create.vue';
export default {
name: 'OnTheFly',
components: {
Modal,
OnTheFlyPerson,
OnTheFlyThirdparty,
OnTheFlyCreate
},
props: ['type', 'id', 'action', 'buttonText'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
action: this.action
}
},
computed: {
action() {
return this.$data.action;
},
classAction() {
switch (this.action) {
case 'show':
return 'bt-show';
case 'edit':
return 'bt-update';
case 'create':
return 'bt-create';
}
},
titleAction() {
switch (this.action) {
case 'show':
return 'action.show';
case 'edit':
return 'action.edit';
case 'create':
return 'action.create';
}
},
titleModal() {
switch (this.action) {
case 'show':
return 'onthefly.show.' + this.type;
case 'edit':
return 'onthefly.edit.' + this.type;
case 'create':
return 'onthefly.create.title';
}
}
},
methods: {
openModal() {
this.modal.showModal = true;
this.$nextTick(function() {
//this.$refs.search.focus();
})
},
changeActionTo(action) {
// [BUG] clic first on show item button; in modal clic edit button; close modal; clic again on show item button
this.$data.action = action;
},
saveAction() {
console.log('saveAction');
if (this.type === 'person') {
this.$refs.castPerson.postData();
} else if (this.type === 'thirdparty') {
this.$refs.castThirdparty.postData();
} else {
// saveAction() ==cast=to==> child.castByType() ==cast=to==> grand-child.postData()
this.$refs.castNew.castByType();
}
this.modal.showModal = false;
}
}
}
</script>
<style lang="css" scoped>
a {
cursor: pointer;
}
</style>

View File

@@ -1,84 +0,0 @@
<template>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ active: isActive('person') }">
<label for="person">
<input type="radio" name="person" v-model="radioType" value="person">
{{ $t('onthefly.create.person') }}
</label>
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: isActive('thirdparty') }">
<label for="thirdparty">
<input type="radio" name="thirdparty" v-model="radioType" value="thirdparty">
{{ $t('onthefly.create.thirdparty') }}
</label>
</a>
</li>
</ul>
<div class="my-4">
<on-the-fly-person
v-if="type === 'person'"
v-bind:action="action"
ref="castPerson">
</on-the-fly-person>
<on-the-fly-thirdparty
v-if="type === 'thirdparty'"
v-bind:action="action"
ref="castThirdparty">
</on-the-fly-thirdparty>
</div>
</template>
<script>
import OnTheFlyPerson from 'ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue';
import OnTheFlyThirdparty from './ThirdParty.vue'; // TODO move in ChillThirdpartyAssets
export default {
name: "OnTheFlyCreate",
props: ['action'],
components: {
OnTheFlyPerson,
OnTheFlyThirdparty
},
data() {
return {
type: 'person' //by default
}
},
computed: {
radioType: {
set(type) {
this.type = type;
},
get() {
return this.type;
}
}
},
methods: {
isActive(tab) {
return (this.type === tab) ? true : false;
},
castByType() {
console.log('saveActionByType');
if (this.type === 'person') {
this.$refs.castPerson.postData();
} else if (this.type === 'thirdparty') {
this.$refs.castThirdparty.postData();
} else {
throw Error('Invalid type of entity');
}
}
}
}
</script>
<style lang="css" scoped>
label {
cursor: pointer;
}
</style>

View File

@@ -1,23 +0,0 @@
<template>
<div v-if="action === 'show'">
show
thirdparty
{{ id }}
</div>
<div v-else-if="action === 'edit' || action === 'create'">
{{ action }}
thirdparty
{{ id }}
</div>
</template>
<script>
export default {
name: "OnTheFlyThirdParty",
props: ['id', 'type', 'action']
}
// TODO move in ChillThirdpartyAssets
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,79 +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",
back: "Retour",
check_all: "cocher tout",
reset: "réinitialiser"
},
nav: {
next: "Suivant",
previous: "Précédent",
top: "Haut",
bottom: "Bas",
},
onthefly: {
show: {
person: "Détails de l'usager",
thirdparty: "Détails du tiers"
},
edit: {
person: "Modifier un usager",
thirdparty: "Modifier un tiers"
},
create: {
button: "Créer \"{q}\"",
title: "Créer à la volée…",
person: "un nouvel usager",
thirdparty: "un nouveau tiers"
},
}
}
};
const _createI18n = (appMessages) => {
Object.assign(messages.fr, appMessages.fr);
return createI18n({
locale: 'fr',
fallbackLocale: 'fr',
datetimeFormats,
messages,
})
};
export { _createI18n }

View File

@@ -1,16 +0,0 @@
<div class="chill_address">
{% if options['has_no_address'] == true and address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% endif %}
<div class="chill_address_address {% if options['multiline'] %}chill_address_address--multiline{% endif %}">
{% if address.street is not empty %}<p class="street street1">{{ address.street }}</p>{% endif %}
{% if address.streetNumber is not empty %}<p class="street street2">{{ address.streetNumber }}</p>{% endif %}
{% if address.postCode is not empty %}
<p class="postalCode"><span class="code">{{ address.postCode.code }}</span> <span class="name">{{ address.postCode.name }}</span></p>
<p class="country">{{ address.postCode.country.name|localize_translatable_string }}</p>
{% endif %}
</div>
{%- if options['with_valid_from'] == true -%}
<span class="address_since">{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }}</span>
{%- endif -%}
</div>

View File

@@ -1,14 +1,13 @@
{%- macro _render(address, options) -%}
{%- set options = { 'with_valid_from' : true }|merge(options|default({})) -%}
{%- set options = { 'has_no_address' : false }|merge(options|default({})) -%}
{%- set options = { 'with_icon' : false }|merge(options|default({})) -%}
<div class="chill_address">
{% if options['has_no_address'] == true and address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% endif %}
<div class="chill_address_address">{% if options['with_icon'] == true %}<i class="fa fa-fw fa-map-marker"></i>{% endif %}
{% if address.street is not empty %}<p class="street street1">{{ address.street }}</p>{% endif %}
{% if address.streetNumber is not empty %}<p class="street street2">{{ address.streetNumber }}</p>{% endif %}
<div class="chill_address_address">
{% if address.streetAddress1 is not empty %}<p class="street street1">{{ address.streetAddress1 }}</p>{% endif %}
{% if address.streetAddress2 is not empty %}<p class="street street2">{{ address.streetAddress2 }}</p>{% endif %}
{% if address.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>
<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,7 +0,0 @@
<div class="timeline">
{% for result in results %}
<div class="timeline-item {% if loop.index0 is even %}even{% else %}odd{% endif %}">
{% include result.template with result.template_data %}
</div>
{% endfor %}
</div>

View File

@@ -1,15 +1,7 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block content %}
<div id="container content">
<div class="grid-8 centered">
<h1>{{ 'Global timeline'|trans }}</h1>
{{ timeline|raw }}
{% if nb_items > paginator.getItemsPerPage %}
{{ chill_pagination(paginator) }}
{% endif %}
</div>
</div>
{% endblock content %}
<div class="timeline">
{% for result in results %}
<div class="timeline-item {% if loop.index0 is even %}even{% else %}odd{% endif %}">
{% include result.template with result.template_data %}
</div>
{% endfor %}
</div>

View File

@@ -69,14 +69,6 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'order' => 0
]);
$menu->addChild($this->translator->trans('Global timeline'), [
'route' => 'chill_center_timeline',
])
->setExtras([
'order' => 10
]
);
if ($this->authorizationChecker->isGranted(ChillExportVoter::EXPORT)) {
$menu->addChild($this->translator->trans('Export Menu'), [
'route' => 'chill_main_export_index'

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

@@ -204,7 +204,7 @@ class SearchProvider
}
public function getResultByName($pattern, $name, $start = 0, $limit = 50,
array $options = array(), $format = 'html')
array $options = array(), $format)
{
$terms = $this->parse($pattern);
$search = $this->getByName($name);

View File

@@ -110,6 +110,8 @@ class AuthorizationHelper
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()) {
@@ -117,7 +119,8 @@ class AuthorizationHelper
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role allow to reach the required role
if ($this->isRoleReached($attribute, $roleScope->getRole())) {
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))){
//if yes, we have a right on something...
// perform check on scope if necessary
if ($entity instanceof HasScopeInterface) {
@@ -146,15 +149,12 @@ class AuthorizationHelper
* and optionnaly Scope
*
* @param User $user
* @param string|Role $role
* @param Role $role
* @param null|Scope $scope
* @return Center[]
*/
public function getReachableCenters(User $user, $role, Scope $scope = null)
public function getReachableCenters(User $user, Role $role, Scope $scope = null)
{
if ($role instanceof Role) {
$role = $role->getRole();
}
$centers = array();
foreach ($user->getGroupCenters() as $groupCenter){
@@ -162,7 +162,8 @@ class AuthorizationHelper
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) {
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))) {
if ($scope === null) {
$centers[] = $groupCenter->getCenter();
break 1;
@@ -180,30 +181,6 @@ class AuthorizationHelper
return $centers;
}
/**
* Filter an array of centers, return only center which are reachable
*
* @param User $user The user
* @param array $centers a list of centers which are going to be filtered
* @param string|Center $role
*/
public function filterReachableCenters(User $user, array $centers, $role): array
{
$results = [];
if ($role instanceof Role) {
$role = $role->getRole();
}
foreach ($centers as $center) {
if ($this->userCanReachCenter($user, $center, $role)) {
$results[] = $center;
}
}
return $results;
}
/**
* Return all reachable scope for a given user, center and role
*
@@ -214,12 +191,8 @@ class AuthorizationHelper
* @param Center $center
* @return Scope[]
*/
public function getReachableScopes(User $user, $role, Center $center)
public function getReachableScopes(User $user, Role $role, Center $center)
{
if ($role instanceof Role) {
$role = $role->getRole();
}
return $this->getReachableCircles($user, $role, $center);
}
@@ -227,15 +200,12 @@ class AuthorizationHelper
* Return all reachable circle for a given user, center and role
*
* @param User $user
* @param string|Role $role
* @param Role $role
* @param Center $center
* @return Scope[]
*/
public function getReachableCircles(User $user, $role, Center $center)
public function getReachableCircles(User $user, Role $role, Center $center)
{
if ($role instanceof Role) {
$role = $role->getRole();
}
$scopes = array();
foreach ($user->getGroupCenters() as $groupCenter){
@@ -245,7 +215,9 @@ class AuthorizationHelper
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
//check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) {
if ($this->isRoleReached($role,
new Role($roleScope->getRole()))) {
$scopes[] = $roleScope->getScope();
}
}
@@ -297,10 +269,10 @@ class AuthorizationHelper
* @param Role $parentRole The role which should give access to $childRole
* @return boolean true if the child role is granted by parent role
*/
protected function isRoleReached($childRole, $parentRole)
protected function isRoleReached(Role $childRole, Role $parentRole)
{
$reachableRoles = $this->roleHierarchy
->getReachableRoleNames([$parentRole]);
->getReachableRoles([$parentRole]);
return in_array($childRole, $reachableRoles);
}

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