Merge branch 'master' into calendar/docgen-add-generation

This commit is contained in:
Julien Fastré 2022-10-20 13:34:21 +02:00
commit 1ec3e176fb
56 changed files with 1728 additions and 100 deletions

View File

@ -16,6 +16,8 @@ and this project adheres to
* [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository
* [person][export] Fixed: some inconsistency with date filter on accompanying courses
* [person][export] Fixed: use left join for related entities in accompanying course aggregators
* [workflow] Feature: allow user to copy and send manually the access link for the workflow
* [workflow] Feature: show the email addresses that received an access link for the workflow
## Test releases
@ -32,6 +34,7 @@ and this project adheres to
* [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty
* [search]: Order of birthdate fields changed in advanced search to avoid confusion.
* [workflow]: Constraint added to workflow (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/675)
* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625)
* [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620)
* [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611)

View File

@ -175,13 +175,6 @@ parameters:
count: 1
path: src/Bundle/ChillActivityBundle/Form/ActivityType.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php
-
message: """
@ -294,14 +287,6 @@ parameters:
count: 1
path: src/Bundle/ChillEventBundle/Form/Type/PickEventType.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillEventBundle/Search/EventSearch.php
-
message: """
#^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\:
@ -390,13 +375,6 @@ parameters:
count: 1
path: src/Bundle/ChillMainBundle/Export/ExportInterface.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillMainBundle/Export/ExportManager.php
-
message: """
@ -754,14 +732,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillReportBundle/Controller/ReportController.php
-
message: """
#^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\:
@ -794,14 +764,6 @@ parameters:
count: 1
path: src/Bundle/ChillReportBundle/Export/Filter/ReportDateFilter.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillReportBundle/Form/ReportType.php
-
message: """
#^Parameter \\$role of method Chill\\\\ReportBundle\\\\Form\\\\ReportType\\:\\:appendScopeChoices\\(\\) has typehint with deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\:
@ -810,14 +772,6 @@ parameters:
count: 1
path: src/Bundle/ChillReportBundle/Form/ReportType.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 1
path: src/Bundle/ChillReportBundle/Search/ReportSearch.php
-
message: """
#^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\:
@ -826,13 +780,6 @@ parameters:
count: 1
path: src/Bundle/ChillReportBundle/Search/ReportSearch.php
-
message: """
#^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:
Use getReachableCircles$#
"""
count: 2
path: src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php
-
message: """

View File

@ -14,6 +14,8 @@ namespace Chill\DocGeneratorBundle\Controller;
use Chill\DocGeneratorBundle\Context\ContextManager;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -84,4 +86,16 @@ class AdminDocGeneratorTemplateController extends CRUDController
return $entity;
}
/**
* @param QueryBuilder $query
*
* @return QueryBuilder|mixed
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
return $query->addSelect('JSON_EXTRACT(e.name, :lang) AS HIDDEN name_lang')
->setParameter('lang', $request->getLocale())
->addOrderBy('name_lang', 'ASC');
}
}

View File

@ -15,14 +15,18 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\RequestStack;
final class DocGeneratorTemplateRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
private RequestStack $requestStack;
public function __construct(EntityManagerInterface $entityManager, RequestStack $requestStack)
{
$this->repository = $entityManager->getRepository(DocGeneratorTemplate::class);
$this->requestStack = $requestStack;
}
public function countByEntity(string $entity): int
@ -71,7 +75,10 @@ final class DocGeneratorTemplateRepository implements ObjectRepository
$builder
->where('t.entity LIKE :entity')
->andWhere($builder->expr()->eq('t.active', "'TRUE'"))
->setParameter('entity', addslashes($entity));
->setParameter('entity', addslashes($entity))
->addSelect('JSON_EXTRACT(t.name, :lang) AS HIDDEN name_lang')
->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale())
->addOrderBy('name_lang', 'ASC');
return $builder
->getQuery()

View File

@ -2,6 +2,14 @@
{% block title 'docgen.Generate a document'|trans %}
{% block js %}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block content %}
<div class="col-md-10 col-xxl">
<h1>{{ block('title') }}</h1>

View File

@ -77,6 +77,19 @@ final class DocGenObjectNormalizerTest extends KernelTestCase
$this->assertArrayNotHasKey('baz', $actual['child']);
}
public function testNormalizableBooleanPropertyOrMethodOnNull()
{
$actual = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableClassWithBool::class]);
$expected = [
'foo' => null,
'thing' => null,
'isNull' => true,
];
$this->assertEquals($expected, $actual);
}
public function testNormalizationBasic()
{
$scope = new Scope();
@ -93,6 +106,22 @@ final class DocGenObjectNormalizerTest extends KernelTestCase
$this->assertEquals($expected, $normalized, 'test normalization fo a scope');
}
public function testNormalizeBooleanPropertyOrMethod()
{
$testable = new TestableClassWithBool();
$testable->foo = false;
$actual = $this->normalizer->normalize($testable, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableClassWithBool::class]);
$expected = [
'foo' => false,
'thing' => true,
'isNull' => false,
];
$this->assertEquals($expected, $actual);
}
public function testNormalizeNull()
{
$actual = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Scope::class]);
@ -170,3 +199,19 @@ class TestableChildClass
*/
public string $foo = 'bar';
}
class TestableClassWithBool
{
/**
* @Serializer\Groups("docgen:read")
*/
public bool $foo;
/**
* @Serializer\Groups("docgen:read")
*/
public function getThing(): bool
{
return true;
}
}

View File

@ -14,7 +14,7 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\PostalCodeRepository;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@ -30,11 +30,11 @@ final class PostalCodeAPIController extends ApiController
private PaginatorFactory $paginatorFactory;
private PostalCodeRepository $postalCodeRepository;
private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct(
CountryRepository $countryRepository,
PostalCodeRepository $postalCodeRepository,
PostalCodeRepositoryInterface $postalCodeRepository,
PaginatorFactory $paginatorFactory
) {
$this->countryRepository = $countryRepository;

View File

@ -27,6 +27,7 @@ use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey;
use Chill\MainBundle\Doctrine\DQL\JsonAggregate;
use Chill\MainBundle\Doctrine\DQL\JsonbArrayLength;
use Chill\MainBundle\Doctrine\DQL\JsonbExistsInArray;
use Chill\MainBundle\Doctrine\DQL\JsonExtract;
use Chill\MainBundle\Doctrine\DQL\OverlapsI;
use Chill\MainBundle\Doctrine\DQL\Replace;
use Chill\MainBundle\Doctrine\DQL\Similarity;
@ -234,6 +235,7 @@ class ChillMainExtension extends Extension implements
'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class,
'AGGREGATE' => JsonAggregate::class,
'REPLACE' => Replace::class,
'JSON_EXTRACT' => JsonExtract::class,
],
'numeric_functions' => [
'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class,

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class JsonExtract extends FunctionNode
{
private $element;
private $keyToExtract;
public function getSql(SqlWalker $sqlWalker)
{
return sprintf('%s->>%s', $this->element->dispatch($sqlWalker), $this->keyToExtract->dispatch($sqlWalker));
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->element = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->keyToExtract = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use function gettype;
use function is_int;
class PostalCodeToIdTransformer implements DataTransformerInterface
{
private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct(PostalCodeRepositoryInterface $postalCodeRepository)
{
$this->postalCodeRepository = $postalCodeRepository;
}
public function reverseTransform($value)
{
if (null === $value || trim('') === $value) {
return null;
}
if (!is_int((int) $value)) {
throw new TransformationFailedException('Cannot transform ' . gettype($value));
}
return $this->postalCodeRepository->find((int) $value);
}
public function transform($value)
{
if (null === $value) {
return null;
}
if ($value instanceof PostalCode) {
return $value->getId();
}
throw new TransformationFailedException('Could not reverseTransform ' . gettype($value));
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickPostalCodeType extends AbstractType
{
private PostalCodeToIdTransformer $postalCodeToIdTransformer;
public function __construct(PostalCodeToIdTransformer $postalCodeToIdTransformer)
{
$this->postalCodeToIdTransformer = $postalCodeToIdTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->postalCodeToIdTransformer);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['uniqid'] = $view->vars['attr']['data-input-postal-code'] = uniqid('input_pick_postal_code_');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', PostalCode::class)
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
}
}

View File

@ -18,10 +18,9 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
use RuntimeException;
final class PostalCodeRepository implements ObjectRepository
final class PostalCodeRepository implements PostalCodeRepositoryInterface
{
private EntityManagerInterface $entityManager;
@ -29,7 +28,7 @@ final class PostalCodeRepository implements ObjectRepository
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PostalCode::class);
$this->repository = $entityManager->getRepository($this->getClassName());
$this->entityManager = $entityManager;
}
@ -51,20 +50,11 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->find($id, $lockMode, $lockVersion);
}
/**
* @return PostalCode[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
@ -95,7 +85,7 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy);
}
public function getClassName()
public function getClassName(): string
{
return PostalCode::class;
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\Persistence\ObjectRepository;
interface PostalCodeRepositoryInterface extends ObjectRepository
{
public function countByPattern(string $pattern, ?Country $country): int;
public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode;
/**
* @return PostalCode[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode;
public function getClassName(): string;
}

View File

@ -0,0 +1,60 @@
import { createApp } from 'vue';
import PickPostalCode from 'ChillMainAssets/vuejs/PickPostalCode/PickPostalCode';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = _createI18n(appMessages);
function loadOnePicker(el, input, uniqId, city) {
const app = createApp({
template: '<pick-postal-code @select-city="onCitySelected" @removeCity="onCityRemoved" :picked="city"></pick-postal-code>',
components: {
PickPostalCode,
},
data() {
return {
city: city,
}
},
methods: {
onCitySelected(city) {
this.city = city;
input.value = city.id;
},
onCityRemoved(city) {
this.city = null;
input.value = '';
}
}
})
.use(i18n)
.mount(el);
}
function loadDynamicPickers(element) {
let apps = element.querySelectorAll('[data-module="pick-postal-code"]');
apps.forEach(function(el) {
const
uniqId = el.dataset.uniqid,
input = document.querySelector(`input[data-input-uniqid="${uniqId}"]`),
cityIdValue = input.value === '' ? null : input.value
;
if (cityIdValue !== null) {
makeFetch('GET', `/api/1.0/main/postal-code/${cityIdValue}.json`).then(city => {
loadOnePicker(el, input, uniqId, city);
})
} else {
loadOnePicker(el, input, uniqId, null);
}
});
}
document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPickers(document)
})

View File

@ -0,0 +1,29 @@
# Pickpostalcode
Allow to pick a postal code.
In use with module `mod_pick_postal_code`, associated with `PickPostalCodeType` in php.
## Usage
`<pick-postal-code @select-city="onCitySelected" @removeCity="onCityRemoved" :picked="city"></pick-postal-code>`
## Props
* `picked`: the city picked. A javascript object (a city). Null if empty.
* `country`: country to restraint search on picked. May be null.
## Emits
### `selectCity`
When a city is onCitySelected.
Argument: a js object, representing a city
### `removeCity`
When a city is removed.
Argument: a js object, representing a city

View File

@ -0,0 +1,107 @@
<template>
<div class="PickPostalCode">
<vue-multiselect
id="citySelector"
@search-change="listenInputSearch"
ref="citySelector"
v-model="internalPicked"
@select="selectCity"
@remove="remove"
name=""
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:taggable="true"
:multiple="false"
:internal-search="false"
:loading="isLoading"
:options="cities"></vue-multiselect>
</div>
</template>
<script lang="js">
import VueMultiselect from "vue-multiselect";
import {reactive, defineProps, onMounted} from "vue";
import {fetchCities, searchCities} from "./api";
export default {
components: {
VueMultiselect,
},
data() {
return {
cities: [],
internalPicked: null,
isLoading: false,
abortControllers: [],
}
},
emits: ['pickCity', 'removeCity'],
props: {
picked: {
type: Object,
required: false,
default: null
},
country: {
type: Object,
required: false,
default: null
}
},
mounted() {
if (this.picked !== null) {
this.internalPicked = this.picked;
this.cities.push(this.picked);
}
},
methods: {
transName(value) {
return (value.code && value.name) ? `${value.name} (${value.code})` : '';
},
selectCity(city) {
this.$emit('selectCity', city);
},
listenInputSearch(query) {
if (query.length <= 2) {
return;
}
let c = this.abortControllers.pop();
while (typeof c !== 'undefined') {
c.abort();
c = this.abortControllers.pop();
}
this.isLoading = true;
let controller = new AbortController();
this.abortControllers.push(controller);
searchCities(query, this.country, controller).then(
newCities => {
this.cities = this.cities.filter(city => city.id === this.picked);
newCities.forEach(item => {
this.cities.push(item);
})
this.isLoading = false;
return Promise.resolve();
})
.catch((error) => {
console.log(error); //TODO better error handling
this.isLoading = false;
});
},
remove(item) {
this.$emit('removeCity', item);
}
},
}
</script>

View File

@ -0,0 +1,3 @@
.PickPostalCode {
}

View File

@ -0,0 +1,43 @@
import {makeFetch, fetchResults} from 'ChillMainAssets/lib/api/apiMethods';
/**
* Endpoint chill_api_single_postal_code__index
* method GET, get Cities Object
* @params {object} a country object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/
const fetchCities = (country) => {
// warning: do not use fetchResults (in apiMethods): we need only a **part** of the results in the db
const params = new URLSearchParams({item_per_page: 100});
if (country !== null) {
params.append('country', country.id);
}
return makeFetch('GET', `/api/1.0/main/postal-code.json?${params.toString()}`).then(r => Promise.resolve(r.results));
};
/**
* Endpoint chill_main_postalcodeapi_search
* method GET, get Cities Object
* @params {string} search a search string
* @params {object} country a country object
* @params {AbortController} an abort controller
* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string
*/
const searchCities = (search, country, controller) => {
const url = '/api/1.0/main/postal-code/search.json?';
const params = new URLSearchParams({q: search});
if (country !== null) {
Object.assign('country', country.id);
}
return makeFetch('GET', url + params, null, {signal: controller.signal})
.then(result => Promise.resolve(result.results));
};
export {
fetchCities,
searchCities,
};

View File

@ -251,3 +251,9 @@
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}
{% block pick_postal_code_widget %}
{{ form_help(form)}}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-postal-code" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}

View File

@ -95,6 +95,15 @@
</ul>
{% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>
{% for e in entity_workflow.currentStep.destEmail -%}
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
{%- endfor %}
</ul>
{% endif %}
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
@ -103,6 +112,21 @@
{% endfor %}
</ul>
{% endif %}
{% if is_granted('CHILL_MAIN_WORKFLOW_LINK_SHOW', entity_workflow) %}
<p><b>{{ 'workflow.This link grant any user to apply a transition'|trans }}&nbsp;:</b></p>
{% set link = absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey})) %}
<div class="input-group mb-3">
<input type="text" readonly value="{{ link|e('html_attr') }}" class="form-control">
<button class="btn btn-secondary" type="button" id="button-copy" onclick="navigator.clipboard.writeText('{{ link|e('html_attr') }}').then(() => { window.alert({{ ('"' ~ 'workflow.Access link copied'|trans ~ ' !"') |e('html_attr') }})});"><i class="fa fa-files-o"></i></button>
<a class="btn btn-secondary" type="button" id="button-email"
href="mailto:?body={{ ((('workflow.The workflow may be accssed through this link'|trans)~':')|e('url')) ~ '%0D%0A%0D%0A' ~ link|e('url') }}"><i class="fa fa-envelope"></i></a>
</div>
{% endif %}
{% endif %}
</div>

View File

@ -81,6 +81,15 @@
</ul>
{% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>
{% for e in entity_workflow.currentStep.destEmail -%}
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
{%- endfor %}
</ul>
{% endif %}
{% if step.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>

View File

@ -190,8 +190,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface
/**
* Return all reachable scope for a given user, center and role.
*
* @deprecated Use getReachableCircles
*
* @param Center|Center[] $center
*
* @return array|Scope[]

View File

@ -27,6 +27,8 @@ class EntityWorkflowVoter extends Voter
public const SEE = 'CHILL_MAIN_WORKFLOW_SEE';
public const SHOW_ENTITY_LINK = 'CHILL_MAIN_WORKFLOW_LINK_SHOW';
private EntityWorkflowManager $manager;
private Security $security;
@ -80,6 +82,19 @@ class EntityWorkflowVoter extends Voter
case self::DELETE:
return $subject->getStep() === 'initial';
case self::SHOW_ENTITY_LINK:
if ($subject->getStep() === 'initial') {
return false;
}
$currentStep = $subject->getCurrentStepChained();
if ($currentStep->isFinal()) {
return false;
}
return $currentStep->getPrevious()->getTransitionBy() === $this->security->getUser();
default:
throw new UnexpectedValueException("attribute {$attribute} not supported");
}
@ -91,6 +106,7 @@ class EntityWorkflowVoter extends Voter
self::SEE,
self::CREATE,
self::DELETE,
self::SHOW_ENTITY_LINK,
];
}
}

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Service\Import;
use Exception;
use League\Csv\Reader;
use League\Csv\Statement;
use RuntimeException;

View File

@ -17,6 +17,7 @@ namespace Chill\MainBundle\Test;
* **Usage : ** You must set up trait with `setUpTrait` before use
* and use tearDownTrait after usage.
*
* @deprecated use @see{\Prophecy\PhpUnit\ProphecyTrait} instead
* @codeCoverageIgnore
*
* @deprecated use @class{Prophecy\PhpUnit\ProphecyTrait} instead

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Doctrine\DQL;
use Chill\MainBundle\Entity\Country;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
final class JsonExtractTest extends KernelTestCase
{
private EntityManagerInterface $em;
protected function setUp(): void
{
self::bootKernel();
$this->em = self::$container->get(EntityManagerInterface::class);
}
public function dataGenerateDql(): iterable
{
yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM ' . Country::class . ' c', []];
yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM ' . Country::class . ' c', ['lang' => 'fr']];
}
/**
* @dataProvider dataGenerateDql
*/
public function testJsonExtract(string $dql, array $args)
{
$results = $this->em->createQuery($dql)
->setMaxResults(2)
->setParameters($args)
->getResult();
$this->assertIsArray($results, 'simply test that the query return a result');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @internal
* @coversNothing
*/
final class PickPostalCodeTypeTest extends TypeTestCase
{
use ProphecyTrait;
public function testSubmitValidData(): void
{
$builder = $this->factory->createBuilder(FormType::class, ['postal_code' => null]);
$builder->add('postal_code', PickPostalCodeType::class);
$form = $builder->getForm();
$form->submit(['postal_code' => '1']);
$this->assertTrue($form->isSynchronized());
$this->assertEquals(1, $form['postal_code']->getData()->getId());
}
protected function getExtensions()
{
$postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class);
$postalCodeRepository->find(Argument::any())
->will(static function ($args) {
$postalCode = new PostalCode();
$reflectionClass = new ReflectionClass($postalCode);
$id = $reflectionClass->getProperty('id');
$id->setAccessible(true);
$id->setValue($postalCode, (int) $args[0]);
return $postalCode;
});
$type = new PickPostalCodeType(
new PostalCodeToIdTransformer(
$postalCodeRepository->reveal()
)
);
return [
new PreloadedExtension([$type], []),
];
}
}

View File

@ -72,7 +72,7 @@ final class ValidPhonenumber extends ConstraintValidator
}
if (false === $isValid) {
$this->context->addViolation($message, ['%phonenumber%' => $value]);
$this->context->addViolation($message, ['%phonenumber%' => $value, '%formatted%' => $this->phonenumberHelper->format($value)]);
}
}
}

View File

@ -70,6 +70,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js');
encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js');
// Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');

View File

@ -454,7 +454,6 @@ workflow:
Delete workflow: Supprimer le workflow
Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ?
You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow.
Those users are also granted to apply a transition by using an access key: Ces utilisateurs peuvent également valider cette étape, grâce à un lien d'accès
dest by email: Liens d'autorisation par email
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Ce lien d'accès permettra à l'utilisateur de valider cette étape.
Add an email: Ajouter une adresse email
@ -466,6 +465,11 @@ workflow:
Previous workflow transitionned help: Workflows où vous avez exécuté une action.
For: Pour
You must select a next step, pick another decision if no next steps are available: Il faut une prochaine étape. Choissisez une autre décision si nécessaire.
An access key was also sent to those addresses: Un lien d'accès a été envoyé à ces addresses
Those users are also granted to apply a transition by using an access key: Ces utilisateurs ont obtennu l'accès grâce au lien reçu par email
Access link copied: Lien d'accès copié
This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition
The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant
Subscribe final: Recevoir une notification à l'étape finale

View File

@ -62,6 +62,7 @@ class UserRefEventSubscriber implements EventSubscriberInterface
&& $period->getUser() !== $this->security->getUser()
&& null !== $period->getUser()
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
&& !$period->isPreventUserIsChangedNotification()
) {
$this->generateNotificationToUser($period);
}

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository;
@ -92,12 +94,14 @@ class ReassignAccompanyingPeriodController extends AbstractController
$form->handleRequest($request);
$userFrom = $form['user']->getData();
$postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : [];
$total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom);
$paginator = $this->paginatorFactory->create($total);
$periods = $this->accompanyingPeriodACLAwareRepository
->findByUserOpenedAccompanyingPeriod(
->findByUserAndPostalCodesOpenedAccompanyingPeriod(
$userFrom,
$postalCodes,
['openingDate' => 'ASC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
@ -123,7 +127,7 @@ class ReassignAccompanyingPeriodController extends AbstractController
$period = $this->courseRepository->find($periodId);
if ($period->getUser() === $userFrom) {
$period->setUser($userTo);
$period->setUser($userTo, true);
}
}
@ -148,7 +152,9 @@ class ReassignAccompanyingPeriodController extends AbstractController
{
$data = [
'user' => null,
'postal_code' => null,
];
$builder = $this->formFactory->createBuilder(FormType::class, $data, [
'method' => 'get', 'csrf_protection' => false, ]);
@ -158,12 +164,17 @@ class ReassignAccompanyingPeriodController extends AbstractController
'label' => 'reassign.Current user',
'required' => false,
'help' => 'reassign.Choose a user and click on "Filter" to apply',
])
->add('postal_code', PickPostalCodeType::class, [
'label' => 'reassign.Filter by postal code',
'required' => false,
'help' => 'reassign.Filter course which are located inside a postal code',
]);
return $builder->getForm();
}
private function buildReassignForm(array $periodIds, ?User $userFrom): FormInterface
private function buildReassignForm(array $periodIds, ?User $userFrom = null): FormInterface
{
$defaultData = [
'userFrom' => $userFrom,

View File

@ -270,6 +270,8 @@ class AccompanyingPeriod implements
*/
private ?Comment $pinnedComment = null;
private bool $preventUserIsChangedNotification = false;
/**
* @ORM\Column(type="text")
* @Groups({"read", "write"})
@ -1077,6 +1079,11 @@ class AccompanyingPeriod implements
return false;
}
public function isPreventUserIsChangedNotification(): bool
{
return $this->preventUserIsChangedNotification;
}
public function isRequestorAnonymous(): bool
{
return $this->requestorAnonymous;
@ -1372,11 +1379,12 @@ class AccompanyingPeriod implements
return $this;
}
public function setUser(?User $user): self
public function setUser(?User $user, bool $preventNotification = false): self
{
if ($this->user !== $user) {
$this->userPrevious = $this->user;
$this->userIsChanged = true;
$this->preventUserIsChangedNotification = $preventNotification;
foreach ($this->userHistories as $history) {
if (null === $history->getEndDate()) {

View File

@ -35,6 +35,11 @@ class MaritalStatus
*/
private array $name;
public function __construct()
{
$this->id = substr(md5(uniqid()), 0, 7);
}
/**
* Get id.
*/

View File

@ -72,6 +72,9 @@ class Evaluation
$this->socialActions = new ArrayCollection();
}
/**
* @internal do use @see{SocialAction::addEvaluation}
*/
public function addSocialAction(SocialAction $socialAction): self
{
if (!$this->socialActions->contains($socialAction)) {
@ -111,6 +114,11 @@ class Evaluation
return $this->url;
}
/**
* @return $this
*
* @internal do use @see{SocialAction::removeEvaluation}
*/
public function removeSocialAction(SocialAction $socialAction): self
{
if ($this->socialActions->contains($socialAction)) {

View File

@ -112,6 +112,7 @@ class SocialAction
{
if (!$this->evaluations->contains($evaluation)) {
$this->evaluations[] = $evaluation;
$evaluation->addSocialAction($this);
}
return $this;
@ -310,6 +311,7 @@ class SocialAction
public function removeEvaluation(Evaluation $evaluation): self
{
$this->evaluations->removeElement($evaluation);
$evaluation->removeSocialAction($this);
return $this;
}

View File

@ -14,7 +14,6 @@ namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\PersonBundle\Entity\MaritalStatus;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@ -26,9 +25,6 @@ class MaritalStatusType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', TextType::class, [
'label' => 'Identifiant',
])
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
]);

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
@ -19,10 +21,14 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
use function count;
@ -49,7 +55,12 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function buildQueryOpenedAccompanyingCourseByUser(?User $user)
/**
* @param array|PostalCode[]
*
* @return QueryBuilder
*/
public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = [])
{
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
@ -65,6 +76,37 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
->setParameter('now', new DateTime('now'))
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT);
if ([] !== $postalCodes) {
$qb->join('ap.locationHistories', 'location_history')
->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)')
->join(
Address::class,
'address',
Join::WITH,
'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id'
)
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('person_address'),
$qb->expr()->andX(
$qb->expr()->lte('person_address.validFrom', ':now'),
$qb->expr()->orX(
$qb->expr()->isNull('person_address.validTo'),
$qb->expr()->lt('person_address.validTo', ':now')
)
)
)
)
->andWhere(
$qb->expr()->isNull('location_history.endDate')
)
->andWhere(
$qb->expr()->in('address.postcode', ':postal_codes')
)
->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE)
->setParameter('postal_codes', $postalCodes);
}
return $qb;
}
@ -77,6 +119,18 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getSingleScalarResult();
}
public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int
{
if (null === $user) {
return 0;
}
return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes)
->select('COUNT(ap)')
->getQuery()
->getSingleScalarResult();
}
public function countByUserOpenedAccompanyingPeriod(?User $user): int
{
if (null === $user) {
@ -158,6 +212,24 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getResult();
}
public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array
{
if (null === $user) {
return [];
}
$qb = $this->buildQueryOpenedAccompanyingCourseByUser($user);
$qb->setFirstResult($offset)
->setMaxResults($limit);
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('ap.' . $field, $direction);
}
return $qb->getQuery()->getResult();
}
/**
* @return array|AccompanyingPeriod[]
*/

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
@ -25,6 +26,11 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
*/
public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int;
/**
* @param array|PostalCode[] $postalCodes
*/
public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int;
public function countByUserOpenedAccompanyingPeriod(?User $user): int;
public function findByPerson(
@ -43,5 +49,10 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
*/
public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array;
/**
* @param array|PostalCode[] $postalCodes
*/
public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array;
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array;
}

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository\SocialWork;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
@ -73,6 +74,14 @@ final class GoalRepository implements ObjectRepository
$qb = $this->buildQueryBySocialActionWithDescendants($action);
$qb->select('g');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('g.desactivationDate'),
$qb->expr()->gt('g.desactivationDate', ':now')
)
)
->setParameter('now', new DateTime('now'));
foreach ($orderBy as $sort => $order) {
$qb->addOrderBy('g.' . $sort, $order);
}

View File

@ -76,7 +76,7 @@
<div class="flex-table">
{% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %}
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period), 'show_address': true } %}
{% endfor %}
</div>
{% endif %}

View File

@ -113,6 +113,16 @@
</div>
</div>
{% endif %}
{% if show_address|default(false) and period.location is not null %}
<div class="wl-row">
<div class="wl-col title"><h3>{{ 'Accompanying course location'|trans }}</h3></div>
<div class="wl-col list">
<p class="wl-item">
{{ period.location|chill_entity_render_string }}
</p>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}

View File

@ -5,11 +5,13 @@
{% block js %}
{{ encore_entry_script_tags('mod_set_referrer') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{{ encore_entry_script_tags('mod_pick_postal_code') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_set_referrer') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{{ encore_entry_link_tags('mod_pick_postal_code') }}
{% endblock %}
{% macro period_meta(period) %}
@ -48,6 +50,8 @@
{{ form_start(form) }}
{{ form_label(form.user ) }}
{{ form_widget(form.user, {'attr': {'class': 'select2'}}) }}
{{ form_label(form.postal_code) }}
{{ form_widget(form.postal_code) }}
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-misc">
@ -87,7 +91,7 @@
<div class="flex-table">
{% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %}
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period), 'show_address': true } %}
{% else %}
{% if userFrom is same as(null) %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Pick a user'|trans }}</p>

View File

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Service\DocGenerator;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
@ -19,46 +18,73 @@ use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use LogicException;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists;
use function count;
class PersonContext implements DocGeneratorContextWithAdminFormInterface
final class PersonContext implements PersonContextInterface
{
private AuthorizationHelperInterface $authorizationHelper;
private BaseContextData $baseContextData;
private CenterResolverManagerInterface $centerResolverManager;
private DocumentCategoryRepository $documentCategoryRepository;
private EntityManagerInterface $em;
private NormalizerInterface $normalizer;
private Security $security;
private bool $showScopes;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(
AuthorizationHelperInterface $authorizationHelper,
BaseContextData $baseContextData,
CenterResolverManagerInterface $centerResolverManager,
DocumentCategoryRepository $documentCategoryRepository,
NormalizerInterface $normalizer,
TranslatableStringHelperInterface $translatableStringHelper,
EntityManagerInterface $em,
NormalizerInterface $normalizer,
ParameterBagInterface $parameterBag,
Security $security,
TranslatorInterface $translator,
BaseContextData $baseContextData
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->documentCategoryRepository = $documentCategoryRepository;
$this->normalizer = $normalizer;
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverManager = $centerResolverManager;
$this->baseContextData = $baseContextData;
$this->documentCategoryRepository = $documentCategoryRepository;
$this->em = $em;
$this->normalizer = $normalizer;
$this->security = $security;
$this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes'];
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
}
public function adminFormReverseTransform(array $data): array
@ -102,9 +128,30 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
'choice_label' => function ($entity = null) {
return $entity ? $this->translatableStringHelper->localize($entity->getName()) : '';
},
'required' => true,
]);
}
/**
* @param Person $entity
*/
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{
$builder->add('title', TextType::class, [
'required' => true,
'label' => 'docgen.Document title',
'data' => $this->translatableStringHelper->localize($template->getName()),
]);
if ($this->isScopeNecessary($entity)) {
$builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverManager->resolveCenters($entity),
'role' => PersonDocumentVoter::CREATE,
'label' => 'Scope',
]);
}
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{
if (!$entity instanceof Person) {
@ -156,6 +203,14 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
return true;
}
/**
* @param Person $entity
*/
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
{
return true;
}
/**
* @param Person $entity
*/
@ -163,7 +218,9 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
{
$doc = new PersonDocument();
$doc->setTemplate($template)
->setTitle($this->translatableStringHelper->localize($template->getName()))
->setTitle(
$contextGenerationData['title'] ?? $this->translatableStringHelper->localize($template->getName())
)
->setDate(new DateTime())
->setDescription($this->translatableStringHelper->localize($template->getName()))
->setPerson($entity)
@ -178,6 +235,36 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
);
}
if ($this->isScopeNecessary($entity)) {
$doc->setScope($contextGenerationData['scope']);
} elseif ($this->showScopes) {
// in this case, it should have only one scope possible, we get it through AuthorizationHelper::getReachableScopes
$scopes = $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::CREATE,
$this->centerResolverManager->resolveCenters($entity)
);
if (1 !== count($scopes)) {
throw new LogicException('at this step, it should have only one scope');
}
$doc->setScope($scopes[0]);
}
$this->em->persist($doc);
}
private function isScopeNecessary(Person $person): bool
{
if ($this->showScopes && 1 < $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::CREATE,
$this->centerResolverManager->resolveCenters($person)
)) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\DocGenerator;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Form\FormBuilderInterface;
interface PersonContextInterface extends DocGeneratorContextWithAdminFormInterface, DocGeneratorContextWithPublicFormInterface
{
public function adminFormReverseTransform(array $data): array;
public function adminFormTransform(array $data): array;
public function buildAdminForm(FormBuilderInterface $builder): void;
/**
* @param Person $entity
*/
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array;
public function getDescription(): string;
public function getEntityClass(): string;
public function getFormData(DocGeneratorTemplate $template, $entity): array;
public function getName(): string;
public function hasAdminForm(): bool;
/**
* @param Person $entity
*/
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
/**
* @param Person $entity
*/
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void;
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\DocGenerator;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Context to generate a document with a destinee (i.e. generate a letter).
*/
class PersonContextWithThirdParty implements DocGeneratorContextWithAdminFormInterface, DocGeneratorContextWithPublicFormInterface
{
private NormalizerInterface $normalizer;
private PersonContextInterface $personContext;
public function __construct(
PersonContextInterface $personContext,
NormalizerInterface $normalizer
) {
$this->personContext = $personContext;
$this->normalizer = $normalizer;
}
public function adminFormReverseTransform(array $data): array
{
return array_merge(
$this->personContext->adminFormReverseTransform($data),
['label' => $data['label']]
);
}
public function adminFormTransform(array $data): array
{
return array_merge(
$this->personContext->adminFormTransform($data),
['label' => $data['label'] ?? '']
);
}
public function buildAdminForm(FormBuilderInterface $builder): void
{
$this->personContext->buildAdminForm($builder);
$builder->add('label', TextType::class, [
'label' => 'docgen.Label for third party',
'required' => true,
]);
}
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{
$this->personContext->buildPublicForm($builder, $template, $entity);
$builder->add('thirdParty', PickThirdpartyDynamicType::class, [
'multiple' => false,
'label' => $template->getOptions()['label'] ?? 'ThirdParty',
'validation_groups' => ['__none__'],
]);
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{
$data = $this->personContext->getData($template, $entity, $contextGenerationData);
$data['thirdParty'] = $this->normalizer->normalize(
$contextGenerationData['thirdParty'],
'docgen',
['docgen:expects' => ThirdParty::class, 'groups' => ['docgen:read']]
);
return $data;
}
public function getDescription(): string
{
return 'docgen.A context for person with a third party (for sending mail)';
}
public function getEntityClass(): string
{
return $this->personContext->getEntityClass();
}
public function getFormData(DocGeneratorTemplate $template, $entity): array
{
return $this->personContext->getFormData($template, $entity);
}
public static function getKey(): string
{
return self::class;
}
public function getName(): string
{
return 'docgen.Person with third party';
}
public function hasAdminForm(): bool
{
return true;
}
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
{
return true;
}
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{
$this->personContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
}
}

View File

@ -193,7 +193,7 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface
/** @var Evaluation $eval */
$eval = $this->getOrCreateEntity($this->evaluationRepository, 'title', ['fr' => $evaluationTitle]);
$eval->setTitle(['fr' => $evaluationTitle]);
$eval->addSocialAction($socialAction);
$socialAction->addEvaluation($eval);
$this->entityManager->persist($eval);

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class AccompanyingPeriodOriginNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$o = new Origin();
$normalized = $this->normalizer->normalize(
$o,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('origin', $normalized['type']);
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class AccompanyingPeriodResourceNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalizeNullHasSameValueAsNotNull()
{
$nullResource = $this->normalizer->normalize(null, 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]);
$notNull = $this->normalizer->normalize(new Resource(), 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]);
$this->assertEqualsCanonicalizing(array_keys($notNull), array_keys($nullResource));
}
public function testNormalizeResource()
{
$resource = new Resource();
$resource
->setComment('blabla')
->setResource(new ThirdParty());
$expected = [
'type' => 'accompanying_period_resource',
'isNull' => false,
'comment' => 'blabla',
];
$actual = $this->normalizer->normalize($resource, 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]);
// we do not test for sub array (person, thirdparty). We then check first for base value...
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$this->assertEquals($value, $actual[$key]);
}
// ... and then for the existence of some values
$this->assertArrayHasKey('person', $actual);
$this->assertArrayHasKey('thirdParty', $actual);
}
}

View File

@ -60,7 +60,7 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
}
}
public function testNormlalize()
public function testNormalize()
{
$work = new AccompanyingPeriodWork();
$work

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class SocialActionNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$sa = new SocialAction();
$normalized = $this->normalizer->normalize(
$sa,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('social_work_social_action', $normalized['type']);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class SocialIssueNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$si = new SocialIssue();
$normalized = $this->normalizer->normalize(
$si,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('social_issue', $normalized['type']);
}
}

View File

@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Service\DocGenerator;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Service\DocGenerator\PersonContext;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Exception\Prediction\FailedPredictionException;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
/**
* @internal
* @coversNothing
*/
final class PersonContextTest extends TestCase
{
use ProphecyTrait;
/**
* Test that the build person context works in the case when 'form_show_scope' is false.
*/
public function testScopeDoNotShowScopeInForms()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$parameter = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => false]]]);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if (null !== $personDocument->getScope()) {
throw new FailedPredictionException('the person document should not have any scope');
}
});
$personContext = $this->buildPersonContext(
null,
null,
null,
null,
$em->reveal(),
null,
$parameter
);
$personContext->buildPublicForm($this->buildFormBuilder(false), $docGen, $person);
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
[]
);
}
public function testScopeScopeMustBeShownInFormsAndUserAccessMultipleScope()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$scope = new Scope();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) use ($scope) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if ($personDocument->getScope() !== $scope) {
throw new FailedPredictionException('the person document should show the exactly prepared scope');
}
});
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
->willReturn([$scope, new Scope()]);
$personContext = $this->buildPersonContext(
$authorizationHelper->reveal(),
null,
null,
null,
$em->reveal(),
);
$personContext->buildPublicForm($this->buildFormBuilder(true), $docGen, $person);
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
['scope' => $scope]
);
}
public function testScopeScopeMustBeShownInFormsAndUserAccessOneScope()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$scope = new Scope();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) use ($scope) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if ($personDocument->getScope() !== $scope) {
throw new FailedPredictionException('the person document should show the exactly prepared scope');
}
});
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
->willReturn([$scope]);
$personContext = $this->buildPersonContext(
$authorizationHelper->reveal(),
null,
null,
null,
$em->reveal(),
);
$personContext->buildPublicForm($this->buildFormBuilder(true), $docGen, $person);
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
['scope' => $scope]
);
}
private function buildFormBuilder(bool $withScope): FormBuilderInterface
{
$builder = $this->prophesize(FormBuilderInterface::class);
$builder->add('title', TextType::class, Argument::type('array'))
->shouldBeCalled(1);
if ($withScope) {
$builder->add('scope', ScopePickerType::class, Argument::type('array'))
->shouldBeCalled();
} else {
$builder->add('scope', ScopePickerType::class, Argument::type('array'))
->shouldNotBeCalled();
}
return $builder->reveal();
}
private function buildPersonContext(
?AuthorizationHelperInterface $authorizationHelper = null,
?BaseContextData $baseContextData = null,
?CenterResolverManagerInterface $centerResolverManager = null,
?DocumentCategoryRepository $documentCategoryRepository = null,
?EntityManagerInterface $em = null,
?NormalizerInterface $normalizer = null,
?ParameterBagInterface $parameterBag = null,
?Security $security = null,
?TranslatorInterface $translator = null,
?TranslatableStringHelperInterface $translatableStringHelper = null
): PersonContext {
if (null === $authorizationHelper) {
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class)->reveal();
}
if (null === $baseContextData) {
$baseContextData = $this->prophesize(BaseContextData::class)->reveal();
}
if (null === $centerResolverManager) {
$centerResolverManager = $this->prophesize(CenterResolverManagerInterface::class);
$centerResolverManager->resolveCenters(Argument::any(), Argument::any())
->willReturn([new Center()]);
$centerResolverManager = $centerResolverManager->reveal();
}
if (null === $documentCategoryRepository) {
$documentCategoryRepository = $this->prophesize(DocumentCategoryRepository::class);
$documentCategoryRepository->find(Argument::type('integer'))->willReturn(
new DocumentCategory(PersonDocument::class, 1)
);
$documentCategoryRepository = $documentCategoryRepository->reveal();
}
if (null === $em) {
$em = $this->prophesize(EntityManagerInterface::class)->reveal();
}
if (null === $normalizer) {
$normalizer = $this->prophesize(NormalizerInterface::class);
$normalizer->normalize(Argument::type(Person::class), 'docgen', Argument::any())
->willReturn(['type' => 'person']);
$normalizer = $normalizer->reveal();
}
if (null === $parameterBag) {
$parameterBag = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => true]]]);
}
if (null === $security) {
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn(new User());
$security = $security->reveal();
}
if (null === $translator) {
$translator = $this->prophesize(TranslatorInterface::class)->reveal();
}
if (null === $translatableStringHelper) {
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
// return only the 'fr' key
$translatableStringHelper->localize(Argument::type('array'))->will(static function ($args) {
return $args[0]['fr'];
});
$translatableStringHelper = $translatableStringHelper->reveal();
}
return new PersonContext(
$authorizationHelper,
$baseContextData,
$centerResolverManager,
$documentCategoryRepository,
$em,
$normalizer,
$parameterBag,
$security,
$translator,
$translatableStringHelper
);
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Service\DocGenerator;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Service\DocGenerator\PersonContextInterface;
use Chill\PersonBundle\Service\DocGenerator\PersonContextWithThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class PersonContextWithThirdPartyTest extends KernelTestCase
{
use ProphecyTrait;
public function testAdminFormReverseTransform()
{
$personContext = $this->buildPersonContextWithThirdParty();
$actual = $personContext->adminFormReverseTransform(['label' => 'bloup']);
$this->assertArrayHasKey('category', $actual);
$this->assertArrayHasKey('label', $actual);
$this->assertEquals('bloup', $actual['label']);
}
public function testAdminFormTransform()
{
$personContext = $this->buildPersonContextWithThirdParty();
$actual = $personContext->adminFormTransform(['label' => 'bloup']);
$this->assertArrayHasKey('from_person', $actual);
$this->assertArrayHasKey('label', $actual);
$this->assertEquals('bloup', $actual['label']);
}
public function testGetData()
{
$personContext = $this->buildPersonContextWithThirdParty();
$actual = $personContext->getData(
(new DocGeneratorTemplate())->setOptions(['label' => 'bloup']),
new Person(),
['thirdParty' => $tp = new ThirdParty()]
);
$this->assertArrayHasKey('person', $actual);
$this->assertArrayHasKey('thirdParty', $actual);
$this->assertEquals(spl_object_hash($tp), $actual['thirdParty']['hash']);
}
private function buildPersonContextWithThirdParty(): PersonContextWithThirdParty
{
$normalizer = $this->prophesize(NormalizerInterface::class);
$normalizer->normalize(Argument::type(ThirdParty::class), 'docgen', Argument::type('array'))
->will(static function ($args): array {
return ['class' => '3party', 'hash' => spl_object_hash($args[0])];
});
$personContext = $this->prophesize(PersonContextInterface::class);
$personContext->adminFormReverseTransform(Argument::type('array'))->willReturn(
['category' => ['idInsideBundle' => 1, 'bundleId' => 'abc']]
);
$personContext->adminFormTransform(Argument::type('array'))->willReturn(
['from_person' => 'kept']
);
$personContext->getData(Argument::type(DocGeneratorTemplate::class), Argument::type(Person::class), Argument::type('array'))
->willReturn(['person' => 'data']);
return new PersonContextWithThirdParty(
$personContext->reveal(),
$normalizer->reveal()
);
}
}

View File

@ -887,6 +887,10 @@ docgen:
A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement
Person basic: Personne (basique)
A basic context for person: Contexte pour les personnes
Person with third party: Personne avec choix d'un tiers
A context for person with a third party (for sending mail): Un contexte d'une personne avec un tiers (pour envoyer un courrier à ce tiers, par exemple)
Label for third party: Label à afficher aux utilisateurs
Document title: Titre du document généré
period_notification:
period_designated_subject: Vous êtes référent d'un parcours d'accompagnement
@ -937,6 +941,8 @@ reassign:
All periods on this list will be reassigned to this user, excepted the one you manually reassigned before: Tous les parcours visibles sur cette page seront assignés à cet utilisateur, sauf ceux que vous aurez assigné à un utilisateur manuellement.
Reassign: Assigner le référent
List periods to be able to reassign them: Choisissez un utilisateur et cliquez sur "Filtrer" pour visualiser ses parcours. Vous pourrez ensuite les réassigner.
Filter by postal code: Filtrer par code postal
Filter course which are located inside a postal code: Afficher uniquement les parcours localisés auprès de ce code postal (une commune peut comporter plusieurs codes postaux).
notification:
Notify referrer: Notifier le référent

View File

@ -214,7 +214,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\Column(name="kind", type="string", length="20", options={"default": ""})
* @Groups({"write"})
* @Groups({"write", "docgen:read", "docgen:read:3party:parent"})
*/
private ?string $kind = '';