merge upgrade-sf5 branch

This commit is contained in:
Julie Lenaerts 2024-07-15 15:16:27 +02:00
commit bc2dfd159c
96 changed files with 1424 additions and 415 deletions

21
.changes/v2.20.0.md Normal file
View File

@ -0,0 +1,21 @@
## v2.20.0 - 2024-06-05
### Fixed
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
* Added translations for choices of durations (> 5 hours)
### Feature
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
This endpoint should be added to make the endpoint works properly:
```yaml
security:
firewalls:
dav:
pattern: ^/dav
provider: chain_provider
stateless: true
guard:
authenticators:
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
```

3
.changes/v2.20.1.md Normal file
View File

@ -0,0 +1,3 @@
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons

31
.changes/v2.21.0.md Normal file
View File

@ -0,0 +1,31 @@
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
* …
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
* …

View File

@ -6,6 +6,64 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
* …
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
* …
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons
## v2.20.0 - 2024-06-05
### Fixed
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
* Added translations for choices of durations (> 5 hours)
### Feature
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
This endpoint should be added to make the endpoint works properly:
```yaml
security:
firewalls:
dav:
pattern: ^/dav
provider: chain_provider
stateless: true
guard:
authenticators:
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
```
## v2.19.0 - 2024-05-14
### Feature
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side

View File

@ -19,7 +19,6 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12",
"knpuniversity/oauth2-client-bundle": "^2.10",
@ -92,12 +91,12 @@
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": ">= 7.5",
"phpunit/phpunit": "^10.5.24",
"rector/rector": "^1.1.0",
"symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4",
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^5.4",
"symfony/phpunit-bridge": "^7.1",
"symfony/runtime": "^5.4",
"symfony/stopwatch": "^5.4",
"symfony/var-dumper": "^5.4"

View File

@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine.
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
or in the :code:`.env.local` file, which should not be commited to the git repository.
or in the :code:`.env.local` file, which should not be committed to the git repository.
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
by the symfony server, from the docker compose services.
@ -114,6 +114,12 @@ you can either:
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
not the password in clear text).
- set up the jwt authentication bundle
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command
:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY`
and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill.
Prepare migrations and other tools
**********************************
@ -164,7 +170,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t
You will require some bundles to have the following development tools:
- add fixtures
- add profiler and var-dumper to debug
- add profiler and debug bundle
Install fixtures
****************
@ -179,7 +185,7 @@ Install fixtures
This will generate user accounts, centers, and some basic configuration.
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
The password is always :code:`password`.
@ -192,7 +198,7 @@ Add web profiler and debugger
.. code-block:: bash
symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle
Working on chill bundles
************************

View File

@ -46,9 +46,11 @@
"@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3",
"@types/dompurify": "^3.0.5",
"dropzone": "^5.7.6",
"es6-promise": "^4.2.8",
"leaflet": "^1.7.1",
"marked": "^12.0.2",
"masonry-layout": "^4.2.2",
"mime": "^4.0.0",
"swagger-ui": "^4.15.5",

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\ActivityBundle\Menu;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new activity in accompanying course', [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
// 'activityType_id' => '',
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 10,
'icon' => 'plus',
])
;
}
}
}

View File

@ -68,7 +68,7 @@
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list">
<p class="wl-item">
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
</p>
</div>
</div>

View File

@ -87,7 +87,8 @@
<li>
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
hello
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span>
{% else %}
{{ _self.insert_onthefly(bloc.type, item) }}
@ -114,7 +115,7 @@
<li>
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span>
{% else %}
{{ _self.insert_onthefly(bloc.type, item) }}
@ -142,7 +143,7 @@
<span class="wl-item">
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
{% set invite = entity.inviteForUser(item) %}
{% if invite is not null %}

View File

@ -41,7 +41,7 @@
{% if activity.user and t.userVisible %}
<li>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
</li>
{% endif %}

View File

@ -37,7 +37,7 @@
{%- if entity.user is not null %}
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd>
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span>
</dd>
{% endif %}

View File

@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
throw new \RuntimeException('Could not determine context of activity.');
}
} elseif ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
return false;
}

View File

@ -77,6 +77,18 @@ Choose a type: Choisir un type
4 hours: 4 heures
4 hours 30: 4 heures 30
5 hours: 5 heures
5 hours 30: 5 heure 30
6 hours: 6 heures
6 hours 30: 6 heure 30
7 hours: 7 heures
7 hours 30: 7 heure 30
8 hours: 8 heures
8 hours 30: 8 heure 30
9 hours: 9 heures
9 hours 30: 9 heure 30
10 hours: 10 heures
11 hours: 11 heures
12 hours: 12 heures
Concerned groups: Parties concernées par l'échange
Persons in accompanying course: Usagers du parcours
Third persons: Tiers non-pro.
@ -210,6 +222,7 @@ Documents label: Libellé du champ Documents
# activity type category admin
ActivityTypeCategory list: Liste des catégories des types d'échange
Create a new activity type category: Créer une nouvelle catégorie de type d'échange
Create a new activity in accompanying course: Créer un échange dans le parcours
# activity delete
Remove activity: Supprimer un échange

View File

@ -49,13 +49,13 @@
<li>
<span>
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
<b>{{ entity.createdBy|chill_entity_render_box }}</b>
<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b>
</span>
</li>
<li>
<span>
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
<b>{{ entity.agent|chill_entity_render_box }}</b>
<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b>
</span>
</li>

View File

@ -18,11 +18,11 @@
<dd>{{ entity.type|chill_entity_render_box }}</dd>
<dt class="inline">{{ 'Created by'|trans }}</dt>
<dd>{{ entity.createdBy }}</dd>
<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd>
<dt class="inline">{{ 'Created for'|trans }}</dt>
<dd>{{ entity.agent }}</dd>
<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd>
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
{%- if entity.location.name is defined -%}
<dd>{{ entity.location.name }}</dd>

View File

@ -72,21 +72,21 @@ days: jours
1 hour 30: 1 heure 30
1 hour 45: 1 heure 45
2 hours: 2 heures
2 hours 30: 2 heure 30
2 hours 30: 2 heures 30
3 hours: 3 heures
3 hours 30: 3 heure 30
3 hours 30: 3 heures 30
4 hours: 4 heures
4 hours 30: 4 heure 30
4 hours 30: 4 heures 30
5 hours: 5 heures
5 hours 30: 5 heure 30
5 hours 30: 5 heures 30
6 hours: 6 heures
6 hours 30: 6 heure 30
6 hours 30: 6 heures 30
7 hours: 7 heures
7 hours 30: 7 heure 30
7 hours 30: 7 heures 30
8 hours: 8 heures
8 hours 30: 8 heure 30
8 hours 30: 8 heures 30
9 hours: 9 heures
9 hours 30: 9 heure 30
9 hours 30: 9 heures 30
10 hours: 10 heures
1/2 day: 1/2 jour
1 day: 1 jour

View File

@ -440,6 +440,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
return $this->startDate;
}
/**
* get the date of the calendar.
*
* Useful for showing the date of the calendar event, required by twig in some places.
*/
public function getDate(): ?\DateTimeImmutable
{
return $this->getStartDate();
}
public function getStatus(): ?string
{
return $this->status;

View File

@ -0,0 +1,48 @@
<?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\CalendarBundle\Menu;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new calendar in accompanying course', [
'route' => 'chill_calendar_calendar_new',
'routeParameters' => [
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 20,
'icon' => 'plus',
])
;
}
}
}

View File

@ -1 +1,2 @@
import './scss/badge.scss';
import './scss/calendar-list.scss';

View File

@ -0,0 +1,26 @@
ul.calendar-list {
list-style-type: none;
padding: 0;
& > li {
display: inline-block;
}
& > li:nth-child(n+2) {
margin-left: 0.25rem;
}
}
div.calendar-list {
ul.calendar-list {
display: inline-block;
}
& > a.calendar-list__global {
display: inline-block;;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);
border-radius: 0.25rem;
text-align: center;
}
}

View File

@ -55,7 +55,7 @@
<div class="item-col">
<ul class="list-content">
{% if calendar.mainUser is not empty %}
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
{% endif %}
</ul>
</div>
@ -132,7 +132,7 @@
<li class="cancel">
<span class="createdBy">
{{ 'Created by'|trans }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
</span>
</li>
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}

View File

@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
switch ($attribute) {
case self::SEE:
case self::CREATE:
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
return false;
}

View File

@ -26,6 +26,7 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim
From the day: Du
to the day: au
Transform to activity: Transformer en échange
Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours
Will send SMS: Un SMS de rappel sera envoyé
Will not send SMS: Aucun SMS de rappel ne sera envoyé
SMS already sent: Un SMS a été envoyé

View File

@ -0,0 +1,65 @@
<?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\DocGeneratorBundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @template T of object
*/
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
{
public function testNullValueHasSameKeysAsNull(): void
{
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
foreach ($normalizedObject as $key => $value) {
if (in_array($key, ['isNull', 'type'])) {
continue;
}
if (is_array($value)) {
if (array_is_list($value)) {
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
} else {
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
}
} elseif (is_string($value)) {
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
}
}
}
/**
* @return T
*/
abstract public function provideNotNullObject(): object;
/**
* @return class-string<T>
*/
abstract public function provideDocGenExpectClass(): string;
abstract public function getNormalizer(): NormalizerInterface;
}

View File

@ -313,4 +313,19 @@ class StoredObject implements Document, TrackCreationInterface
return $this;
}
public function saveHistory(): void
{
if ('' === $this->getFilename()) {
return;
}
$this->datas['history'][] = [
'filename' => $this->getFilename(),
'iv' => $this->getIv(),
'key_infos' => $this->getKeyInfos(),
'type' => $this->getType(),
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
];
}
}

View File

@ -57,8 +57,8 @@ class StoredObjectDataMapper implements DataMapperInterface
/** @var StoredObject $viewData */
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
// we do not want to erase the previous object
$viewData = new StoredObject();
// we want to keep the previous history
$viewData->saveHistory();
}
$viewData->setFilename($forms['stored_object']->getData()['filename']);

View File

@ -4,13 +4,13 @@
Actions
</button>
<ul class="dropdown-menu">
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'">
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
</li>
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
</li>
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'">
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
</li>
<li v-if="props.canDownload">

View File

@ -13,7 +13,7 @@ import {reactive} from "vue";
import {StoredObject, StoredObjectCreated} from "../../types";
interface ConvertButtonConfig {
storedObject: StoredObject|StoredObjectCreated,
storedObject: StoredObject,
classes: { [key: string]: boolean},
filename?: string,
};

View File

@ -11,7 +11,7 @@ import {build_wopi_editor_link} from "./helpers";
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
interface WopiEditButtonConfig {
storedObject: StoredObject|StoredObjectCreated,
storedObject: StoredObject,
returnPath?: string,
classes: {[k: string] : boolean},
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,

View File

@ -0,0 +1,53 @@
<?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\DocStoreBundle\Tests\Entity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectTest extends KernelTestCase
{
public function testSaveHistory(): void
{
$storedObject = new StoredObject();
$storedObject
->setFilename('test_0')
->setIv([2, 4, 6, 8])
->setKeyInfos(['key' => ['data0' => 'data0']])
->setType('text/html');
$storedObject->saveHistory();
$storedObject
->setFilename('test_1')
->setIv([8, 10, 12])
->setKeyInfos(['key' => ['data1' => 'data1']])
->setType('text/text');
$storedObject->saveHistory();
self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']);
self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']);
self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']);
self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']);
self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']);
self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']);
self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']);
self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']);
}
}

View File

@ -56,14 +56,14 @@ class StoredObjectTypeTest extends TypeTestCase
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
JSON];
$model = new StoredObject();
$originalObjectId = spl_object_id($model);
$originalObjectId = spl_object_hash($model);
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$model = $form->getData();
$this->assertNotEquals($originalObjectId, spl_object_hash($model));
$this->assertEquals($originalObjectId, spl_object_hash($model));
$this->assertEquals('abcdef', $model->getFilename());
$this->assertEquals([10, 15, 20, 30], $model->getIv());
$this->assertEquals('text/html', $model->getType());

View File

@ -632,7 +632,7 @@ class ExportController extends AbstractController
}
}
private function rebuildRawData(string $key): array
private function rebuildRawData(?string $key): array
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');

View File

@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->mainLocation;
}
public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->scopeHistories as $scopeHistory) {
if ($at >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
if ($atDate >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate()
)) {
return $scopeHistory->getScope();
}
@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->salt;
}
public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->jobHistories as $jobHistory) {
if ($at >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
if ($atDate >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate()
)) {
return $jobHistory->getJob();
}
@ -285,6 +285,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->jobHistories;
}
public function getUserScopeHistories(): Collection
{
return $this->scopeHistories;
}
/**
* @return ArrayCollection|UserJobHistory[]
*/

View File

@ -0,0 +1,25 @@
<?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\Export;
/**
* Transform data from filter.
*
* This interface defines a method for transforming filter's form data before it is processed.
*
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
* and replace it with some default values, or new default values.
*/
interface DataTransformerInterface
{
public function transformData(?array $before): array;
}

View File

@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface
/**
* Get the default data, that can be use as "data" for the form.
*
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
* transforme the filters's data saved in an export to the desired state.
*/
public function getFormDefaultData(): array;

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
@ -19,9 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AggregatorType extends AbstractType
{
public function __construct() {}
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$exportManager = $options['export_manager'];
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
@ -32,17 +32,24 @@ class AggregatorType extends AbstractType
'required' => false,
]);
$filterFormBuilder = $builder->create('form', FormType::class, [
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
'compound' => true,
'required' => false,
'error_bubbling' => false,
]);
$aggregator->buildForm($filterFormBuilder);
$aggregator->buildForm($aggregatorFormBuilder);
$builder->add($filterFormBuilder);
if ($aggregator instanceof DataTransformerInterface) {
$aggregatorFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $aggregator->transformData($data),
));
}
$builder->add($aggregatorFormBuilder);
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('aggregator_alias')
->setRequired('export_manager')

View File

@ -11,8 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
@ -41,6 +43,13 @@ class FilterType extends AbstractType
]);
$filter->buildForm($filterFormBuilder);
if ($filter instanceof DataTransformerInterface) {
$filterFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $filter->transformData($data),
));
}
$builder->add($filterFormBuilder);
}

View File

@ -43,7 +43,14 @@ export const download_report = (url, container) => {
content = URL.createObjectURL(blob);
}
extension = mime.getExtension(type);
const extensions = new Map();
extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx');
extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods');
extensions.set('application/vnd.ms-excel', 'xlsx');
extensions.set('text/csv', 'csv');
extensions.set('text/csv; charset=utf-8', 'csv');
extension = extensions.get(type);
link.appendChild(document.createTextNode(download_text));
link.classList.add("btn", "btn-action");
@ -55,7 +62,7 @@ export const download_report = (url, container) => {
container.innerHTML = "";
container.appendChild(link);
}).catch(function(error) {
console.log(error);
console.error(error);
var problem_text =
document.createTextNode("Problem during download");

View File

@ -12,5 +12,5 @@ window.addEventListener("DOMContentLoaded", function(e) {
container = document.querySelector("#download_container")
;
download_report(export_generate_url + "?" + query.toString(), container);
download_report(export_generate_url + query.toString(), container);
});

View File

@ -139,7 +139,7 @@ const postprocess = (html: string): string => {
}
const convertMarkdownToHtml = (markdown: string): string => {
marked.use({'hooks': {postprocess, preprocess}});
marked.use({'hooks': {postprocess, preprocess}, 'async': false});
const rawHtml = marked(markdown) as string;
return rawHtml;
};

View File

@ -40,10 +40,10 @@
{{ 'by_user'|trans ~ ' ' }}
{% endif %}
<span class="user">
{{ user|chill_entity_render_box(options['user']) }}
{{ user|chill_entity_render_box({'at_date': comment.date}) }}
</span>
{% endif %}
</div>
{% endif %}
</blockquote>
{{ closing_box|raw }}
{{ closing_box|raw }}

View File

@ -1,10 +1,10 @@
<span class="chill-entity entity-user">
{{- user.label }}
{%- if opts['user_job'] and user.userJob(opts['at']) is not null %}
<span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span>
{%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %}
<span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span>
{%- endif -%}
{%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %}
<span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span>
{%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %}
<span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span>
{%- endif -%}
{%- if opts['absence'] and user.isAbsent %}
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>

View File

@ -0,0 +1,20 @@
{% if menus|length > 0 %}
<li class="dropdown">
<a class="dropdown-toggle btn btn-sm btn-outline-primary"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa fa-flash"></i>
</a>
<div class="dropdown-menu">
{% for menu in menus %}
<a class="dropdown-item"
href="{{ menu.uri }}"
><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i>
{{ menu.label|trans }}
</a>
{% endfor %}
</div>
</li>
{% endif %}

View File

@ -21,7 +21,7 @@
</span>
{% if not c.notification.isSystem %}
<span class="badge-user">
{{ c.notification.sender|chill_entity_render_string }}
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{% else %}
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
@ -53,7 +53,7 @@
{% endif %}
{% for a in c.notification.addressees %}
<span class="badge-user">
{{ a|chill_entity_render_string }}
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{% endfor %}
{% for a in c.notification.addressesEmails %}

View File

@ -1,7 +1,7 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'notification.show notification from %sender%'|trans(
{ '%sender%': notification.sender|chill_entity_render_string }
{ '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) }
) ~ ' ' ~ notification.title %}
{% block js %}

View File

@ -31,14 +31,14 @@
<div class="row">
<div class="col-sm-12">
{{ 'By'|trans }}
{{ step.previous.transitionBy|chill_entity_render_box }},
{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }},
{{ step.previous.transitionAt|format_datetime('short', 'short') }}
</div>
</div>
{% else %}
<div class="row">
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div>
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div>
</div>
<div class="row">
<div class="col-sm-4">{{ 'Le'|trans }}</div>
@ -110,8 +110,8 @@
{% 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>
{% for u in entity_workflow.currentStep.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
{% for u in entity_workflow.currentStepChained.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@ -42,7 +42,7 @@
<div class="item-col" style="width: inherit;">
{% if step.transitionBy is not null %}
<div>
{{ step.transitionBy|chill_entity_render_box }}
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
</div>
{% endif %}
<div>
@ -76,7 +76,7 @@
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.destUser %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}
@ -85,7 +85,7 @@
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}
@ -103,7 +103,7 @@
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
{% for u in step.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@ -3,7 +3,7 @@
{% if step.previous is not null %}
<li>
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
@ -12,19 +12,19 @@
<li>
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
<b>
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
<li>
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
<b>
{% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>

View File

@ -11,8 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Routing;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
@ -20,10 +18,8 @@ use Twig\TwigFunction;
/**
* Add the filter 'chill_menu'.
*/
class MenuTwig extends AbstractExtension implements ContainerAwareInterface
class MenuTwig extends AbstractExtension
{
private ?ContainerInterface $container = null;
/**
* the default parameters for chillMenu.
*
@ -84,9 +80,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface
{
return 'chill_menu';
}
public function setContainer(?ContainerInterface $container = null)
{
$this->container = $container;
}
}

View File

@ -52,7 +52,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz
$data['transitionPreviousBy'] = $this->normalizer->normalize(
$previous->getTransitionBy(),
$format,
$context
[...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()]
);
$data['transitionPreviousAt'] = $this->normalizer->normalize(
$previous->getTransitionAt(),

View File

@ -45,7 +45,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
'message' => $object->getMessage(),
'relatedEntityClass' => $object->getRelatedEntityClass(),
'relatedEntityId' => $object->getRelatedEntityId(),
'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]),
'title' => $object->getTitle(),
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
];

View File

@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\Entity\UserRender;
use libphonenumber\PhoneNumber;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
{
use NormalizerAwareTrait;
final public const AT_DATE = 'chill:user:at_date';
final public const NULL_USER = [
'type' => 'user',
'id' => '',
@ -38,10 +41,16 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'isAbsent' => false,
];
public function __construct(private readonly UserRender $userRender) {}
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
/**
* @param mixed|null $format
*
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
*/
public function normalize($object, $format = null, array $context = [])
{
/** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */
/** @var User $object */
$userJobContext = array_merge(
$context,
@ -72,18 +81,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
}
$at = $context[self::AT_DATE] ?? $this->clock->now();
if ($at instanceof \DateTime) {
$at = \DateTimeImmutable::createFromMutable($at);
}
$data = [
'type' => 'user',
'id' => $object->getId(),
'username' => $object->getUsername(),
'text' => $this->userRender->renderString($object, []),
'text' => $this->userRender->renderString($object, ['at_date' => $at]),
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
'label' => $object->getLabel(),
'email' => (string) $object->getEmail(),
'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext),
'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext),
'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext),
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext),
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
'isAbsent' => $object->isAbsent(),
];

View File

@ -12,8 +12,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use DateTime;
use DateTimeImmutable;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
/**
* @implements ChillEntityRenderInterface<User>
@ -24,15 +30,31 @@ class UserRender implements ChillEntityRenderInterface
'main_scope' => true,
'user_job' => true,
'absence' => true,
'at' => null,
'at_date' => null, // instanceof DateTimeInterface
];
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly \Twig\Environment $engine,
private readonly TranslatorInterface $translator,
private readonly ClockInterface $clock,
) {}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderBox($entity, array $options): string
{
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
if (null === $opts['at_date']) {
$opts['at_date'] = $this->clock->now();
} elseif ($opts['at_date'] instanceof \DateTime) {
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
}
return $this->engine->render('@ChillMain/Entity/user.html.twig', [
'user' => $entity,
'opts' => $opts,
@ -43,16 +65,24 @@ class UserRender implements ChillEntityRenderInterface
{
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
$str = $entity->getLabel();
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getUserJob($opts['at'])->getLabel()).')';
if (null === $opts['at_date']) {
$opts['at_date'] = $this->clock->now();
} elseif ($opts['at_date'] instanceof \DateTime) {
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
}
if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) {
$str = $entity->getLabel();
if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getMainScope($opts['at'])->getName()).')';
->localize($entity->getUserJob($opts['at_date'])->getLabel()).')';
}
if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getMainScope($opts['at_date'])->getName()).')';
}
if ($entity->isAbsent() && $opts['absence']) {

View File

@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase
.'is a string or an be converted to a string', $key)
);
$this->assertTrue(
// conditions
\is_string((string) \call_user_func($closure, '_header'))
&& !empty(\call_user_func($closure, '_header'))
&& '_header' !== \call_user_func($closure, '_header'),
// message
sprintf('Test that the callable return by `getLabels` for key %s '
.'can provide an header', $key)
);
$head = \call_user_func($closure, '_header');
self::assertIsString($head);
self::assertNotEquals('', $head);
self::assertNotEquals('_header', $head);
}
}

View File

@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase
$userRender = $this->prophesize(UserRender::class);
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');
$normalizer = new UserNormalizer($userRender->reveal());
$clock = new MockClock(new \DateTimeImmutable('now'));
$normalizer = new UserNormalizer($userRender->reveal(), $clock);
$normalizer->setNormalizer(new class () implements NormalizerInterface {
public function normalize($object, ?string $format = null, array $context = [])
{

View File

@ -0,0 +1,109 @@
<?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 Templating\Entity;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class UserRenderTest extends TestCase
{
use ProphecyTrait;
public function testRenderUserWithJobAndScopeAtCertainDate(): void
{
// Create a user with a certain user job
$user = new User();
$userJobA = new UserJob();
$scopeA = new Scope();
$userJobA->setLabel(['fr' => 'assistant social'])
->setActive(true);
$scopeA->setName(['fr' => 'service A']);
$user->setLabel('BOB ISLA');
$userJobB = new UserJob();
$scopeB = new Scope();
$userJobB->setLabel(['fr' => 'directrice'])
->setActive(true);
$scopeB->setName(['fr' => 'service B']);
$userJobHistoryA = (new User\UserJobHistory())
->setUser($user)
->setJob($userJobA)
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
$userScopeHistoryA = (new User\UserScopeHistory())
->setUser($user)
->setScope($scopeA)
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
$userJobHistoryB = (new User\UserJobHistory())
->setUser($user)
->setJob($userJobB)
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
$userScopeHistoryB = (new User\UserScopeHistory())
->setUser($user)
->setScope($scopeB)
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
$user->getUserJobHistories()->add($userJobHistoryA);
$user->getUserScopeHistories()->add($userScopeHistoryA);
$user->getUserJobHistories()->add($userJobHistoryB);
$user->getUserScopeHistories()->add($userScopeHistoryB);
// Create renderer
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
$engineMock = $this->createMock(Environment::class);
$translatorMock = $this->createMock(TranslatorInterface::class);
$clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00'));
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
$optionsNoDate['at_date'] = null;
$options['at_date'] = new \DateTime('2023-11-25 12:00:00');
$optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00');
// Check that the user render for the first activity corresponds with the first user job
$expectedStringA = 'BOB ISLA (assistant social) (service A)';
$this->assertEquals($expectedStringA, $renderer->renderString($user, $options));
// Check that the user render for the second activity corresponds with the second user job
$expectedStringB = 'BOB ISLA (directrice) (service B)';
$this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo));
// Check that the user renders the job and scope that is active now, when no date is given
$expectedStringC = 'BOB ISLA (directrice) (service B)';
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
}
}

View File

@ -20,9 +20,5 @@ services:
chill.main.twig.chill_menu:
class: Chill\MainBundle\Routing\MenuTwig
arguments:
- "@chill.main.menu_composer"
calls:
- [setContainer, ["@service_container"]]
tags:
- { name: twig.extension }

View File

@ -707,19 +707,24 @@ class AccompanyingPeriod implements
public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection
{
$today = new \DateTimeImmutable('today');
$criteria = Criteria::create()
->where(Criteria::expr()->gte('startDate', $today))
// ->andWhere(Criteria::expr()->memberOf('persons', $person))
->orderBy(['startDate' => 'DESC'])
->setMaxResults($limit * 2);
return $this->calendars->matching($criteria)
->matching(
// due to a bug, filter two times
Criteria::create()
->where(Criteria::expr()->memberOf('persons', $person))
->setMaxResults($limit)
);
$criteria = Criteria::create();
$expr = Criteria::expr();
$criteria
->where(
$expr->gte('startDate', $today),
)
->orderBy(['startDate' => 'ASC']);
$criteriaByPerson = Criteria::create();
$criteriaByPerson
->where(
$expr->memberOf('persons', $person)
)
->setMaxResults($limit);
return $this->calendars->matching($criteria)->matching($criteriaByPerson);
}
/**
@ -1332,6 +1337,16 @@ class AccompanyingPeriod implements
return $this;
}
public function getUserHistories(): ReadableCollection
{
return $this->userHistories;
}
public function getCurrentUserHistory(): ?UserHistory
{
return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate());
}
private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self
{
if (!$this->stepHistories->contains($stepHistory)) {

View File

@ -42,9 +42,10 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @var Collection<AccompanyingPeriodWorkEvaluation>
*
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
* @internal the serialization for write evaluations is handled in `accompanyingperiodworkdenormalizer`
* @internal the serialization for context docgen:read is handled in `accompanyingperiodworknormalizer`
*/
#[Serializer\Groups(['read', 'docgen:read'])]
#[Serializer\Groups(['read'])]
#[ORM\OneToMany(targetEntity: AccompanyingPeriodWorkEvaluation::class, mappedBy: 'accompanyingPeriodWork', cascade: ['remove', 'persist'], orphanRemoval: true)]
#[ORM\OrderBy(['startDate' => \Doctrine\Common\Collections\Criteria::DESC, 'id' => 'DESC'])]
private Collection $accompanyingPeriodWorkEvaluations;
@ -291,18 +292,20 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @return ReadableCollection<int, User>
*/
#[Serializer\Groups(['read', 'docgen:read', 'read:accompanyingPeriodWork:light', 'accompanying_period_work:edit', 'accompanying_period_work:create'])]
#[Serializer\Groups(['accompanying_period_work:edit'])]
public function getReferrers(): ReadableCollection
{
$users = $this->referrersHistory
->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate())
->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser())
->getValues()
;
->getValues();
return new ArrayCollection(array_values($users));
}
/**
* @return Collection<int, AccompanyingPeriodWorkReferrerHistory>
*/
public function getReferrersHistory(): Collection
{
return $this->referrersHistory;
@ -470,9 +473,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function setCreatedBy(?User $createdBy): self
public function setCreatedBy(?User $user): self
{
$this->createdBy = $createdBy;
$this->createdBy = $user;
return $this;
}
@ -514,14 +517,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
public function setStartDate(\DateTimeInterface $startDate): self
{
$this->startDate = $startDate;
$this->startDate = $startDate instanceof \DateTime ? \DateTimeImmutable::createFromMutable($startDate) : $startDate;
return $this;
}
public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface
{
$this->updatedAt = $datetime;
$this->updatedAt = $datetime instanceof \DateTime ? \DateTimeImmutable::createFromMutable($datetime) : $datetime;
return $this;
}

View File

@ -132,6 +132,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function getCreatedBy(): ?User
{
return $this->getCreator();
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Service\RollingDate\RollingDate;
@ -21,13 +22,17 @@ use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class ReferrerAggregator implements AggregatorInterface
final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface
{
private const A = 'acp_ref_agg_uhistory';
private const P = 'acp_ref_agg_date';
public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter) {}
public function __construct(
private UserRepository $userRepository,
private UserRender $userRender,
private RollingDateConverterInterface $rollingDateConverter
) {}
public function addRole(): ?string
{
@ -44,18 +49,16 @@ final readonly class ReferrerAggregator implements AggregatorInterface
$qb->expr()->orX(
$qb->expr()->isNull(self::A),
$qb->expr()->andX(
$qb->expr()->lte(self::A.'.startDate', ':'.self::P),
$qb->expr()->lt(self::A.'.startDate', ':'.self::P.'_end_date'),
$qb->expr()->orX(
$qb->expr()->isNull(self::A.'.endDate'),
$qb->expr()->gt(self::A.'.endDate', ':'.self::P)
$qb->expr()->gte(self::A.'.endDate', ':'.self::P.'_start_date')
)
)
)
)
->setParameter(
self::P,
$this->rollingDateConverter->convert($data['date_calc'])
);
->setParameter(':'.self::P.'_end_date', $this->rollingDateConverter->convert($data['end_date']))
->setParameter(':'.self::P.'_start_date', $this->rollingDateConverter->convert($data['end_date']));
}
public function applyOn(): string
@ -66,15 +69,37 @@ final readonly class ReferrerAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_calc', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Computation date for referrer',
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Referrer after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return ['date_calc' => new RollingDate(RollingDate::T_TODAY)];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
if (null === $before) {
return $default;
}
$data['start_date'] = $before['date_calc'] ?? $before['start_date'] ?? $default['start_date'];
$data['end_date'] = $before['date_calc'] ?? $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
readonly class ReferrerScopeAggregator implements AggregatorInterface
readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface
{
private const PREFIX = 'acp_agg_referrer_scope';
public function __construct(
private ScopeRepositoryInterface $scopeRepository,
private TranslatableStringHelperInterface $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
@ -46,11 +51,16 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
// check that the user is referrer when the accompanying period is opened
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
),
$qb->expr()->andX(
"{$p}_userHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
@ -66,9 +76,15 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
)
),
$qb->expr()->andX(
"{$p}_scopeHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_scopeHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select")
->addGroupBy("{$p}_select");
}
@ -78,11 +94,36 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder) {}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_scope.Referrer and scope after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_scope.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserJobRepository;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class UserJobAggregator implements AggregatorInterface
final readonly class UserJobAggregator implements AggregatorInterface, DataTransformerInterface
{
private const PREFIX = 'acp_agg_user_job';
public function __construct(
private UserJobRepository $jobRepository,
private TranslatableStringHelper $translatableStringHelper
private TranslatableStringHelper $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
@ -51,6 +56,10 @@ final readonly class UserJobAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
),
$qb->expr()->andX(
"{$p}_userHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
@ -66,9 +75,15 @@ final readonly class UserJobAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_jobHistory.endDate"),
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
)
),
$qb->expr()->andX(
"{$p}_jobHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_jobHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
->addSelect("IDENTITY({$p}_jobHistory.job) AS {$p}_select")
->addGroupBy("{$p}_select");
}
@ -78,11 +93,36 @@ final readonly class UserJobAggregator implements AggregatorInterface
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder) {}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_job.Referrer and job after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_job.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@ -54,7 +54,6 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
'socialAction',
'socialIssue',
'acp_id',
'acp_user',
'startDate',
'endDate',
'goalsId',
@ -70,8 +69,8 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
'personsName',
'thirdParties',
'handlingThierParty',
// 'acpwReferrers',
'referrers',
'acpwReferrers',
'referrer',
'createdAt',
'createdBy',
'updatedAt',
@ -156,9 +155,9 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
[]
);
},
'createdBy', 'updatedBy', 'acp_user' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'referrers' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
// 'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.' . $key),
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'referrer' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
@ -272,8 +271,7 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
// join acp
$qb
->addSelect('acp.id AS acp_id')
->addSelect('IDENTITY(acp.user) AS acp_user');
->addSelect('acp.id AS acp_id');
// persons
$qb
@ -282,21 +280,18 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
// referrers => at date XXXX
$qb
->addSelect('(SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrers');
/*
// acpwReferrers at date XXX
// referrer => at date XXXX
$qb
->addSelect('(
SELECT IDENTITY(acpw_ref_history.accompanyingPeriodWork) AS acpw_ref_history_id,
JSON_BUILD_OBJECT(\'uid\', IDENTITY(acpw_ref_history.user), \'d\', acpw_ref_history.startDate)
FROM ' . AccompanyingPeriodWorkReferrerHistory::class . ' acpw_ref_history ' .
'WHERE acpw_ref_history.accompanyingPeriodWork = acpw AND acpw_ref_history.startDate <= :calcDate AND (acpw_ref_history.endDate IS NULL or acpw_ref_history.endDate > :calcDate) GROUP BY acpw_ref_history_id) AS acpwReferrers'
);
*/
SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrer');
// acpwReferrer at date XXX
$qb->addSelect('(SELECT AGGREGATE(IDENTITY(acpwrh.user)) FROM '.AccompanyingPeriodWorkReferrerHistory::class.' acpwrh
WHERE acpwrh.accompanyingPeriodWork = acpw
AND acpwrh.startDate <= :calcDate AND (acpwrh.endDate IS NULL or acpwrh.endDate > :calcDate)
) AS acpwReferrers');
$qb->setParameter('calcDate', $calcDate);
// thirdparties
$qb

View File

@ -54,7 +54,6 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
'socialAction',
'socialIssue',
'acp_id',
'acp_user',
'startDate',
'endDate',
'goalsId',
@ -70,8 +69,8 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
'personsName',
'thirdParties',
'handlingThierParty',
// 'acpwReferrers',
'referrers',
'acpwReferrers',
'referrer',
'createdAt',
'createdBy',
'updatedAt',
@ -156,9 +155,9 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
[]
);
},
'createdBy', 'updatedBy', 'acp_user' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'referrers' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
// 'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.' . $key),
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'referrer' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
@ -267,8 +266,7 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
// join acp
$qb
->addSelect('acp.id AS acp_id')
->addSelect('IDENTITY(acp.user) AS acp_user');
->addSelect('acp.id AS acp_id');
// persons
$qb
@ -277,21 +275,17 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
// referrers => at date XXXX
// referrer => at date XXXX
$qb
->addSelect('(SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrers');
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrer');
/*
// acpwReferrers at date XXX
$qb
->addSelect('(
SELECT IDENTITY(acpw_ref_history.accompanyingPeriodWork) AS acpw_ref_history_id,
JSON_BUILD_OBJECT(\'uid\', IDENTITY(acpw_ref_history.user), \'d\', acpw_ref_history.startDate)
FROM ' . AccompanyingPeriodWorkReferrerHistory::class . ' acpw_ref_history ' .
'WHERE acpw_ref_history.accompanyingPeriodWork = acpw AND acpw_ref_history.startDate <= :calcDate AND (acpw_ref_history.endDate IS NULL or acpw_ref_history.endDate > :calcDate) GROUP BY acpw_ref_history_id) AS acpwReferrers'
);
*/
$qb->addSelect('(SELECT AGGREGATE(IDENTITY(acpwrh.user)) FROM '.AccompanyingPeriodWorkReferrerHistory::class.' acpwrh
WHERE acpwrh.accompanyingPeriodWork = acpw
AND acpwrh.startDate <= :calcDate AND (acpwrh.endDate IS NULL or acpwrh.endDate > :calcDate)
) AS acpwReferrers');
$qb->setParameter('calcDate', $calcDate);
// thirdparties
$qb

View File

@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
class UserJobFilter implements FilterInterface
final readonly class UserJobFilter implements FilterInterface, DataTransformerInterface
{
private const PREFIX = 'acp_filter_user_job';
public function __construct(
private readonly TranslatableStringHelper $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository,
private TranslatableStringHelper $translatableStringHelper,
private UserJobRepositoryInterface $userJobRepository,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
@ -41,42 +46,31 @@ class UserJobFilter implements FilterInterface
{
$p = self::PREFIX;
$qb
->leftJoin(
'acp.userHistories',
"{$p}_userHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
)
)
)
->leftJoin(
UserJobHistory::class,
"{$p}_jobHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_jobHistory.user", "{$p}_userHistory.user"),
$qb->expr()->andX(
$qb->expr()->lte("{$p}_jobHistory.startDate", "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_jobHistory".'.endDate'),
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_jobHistory.job", ":{$p}_job"))
->setParameter(
"{$p}_job",
$data['jobs'],
$qb->andWhere(
$qb->expr()->exists(
sprintf(
<<<DQL
SELECT 1
FROM %s {$p}_userHistory
JOIN %s {$p}_userJobHistory
WITH
{$p}_userHistory.user = {$p}_userJobHistory.user
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userJobHistory.startDate, {$p}_userJobHistory.endDate) = TRUE
WHERE {$p}_userHistory.accompanyingPeriod = acp
AND {$p}_userHistory.startDate <= :{$p}_endDate
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
AND {$p}_userJobHistory.startDate <= :{$p}_endDate
AND ({$p}_userJobHistory.endDate IS NULL OR {$p}_userJobHistory.endDate > :{$p}_startDate)
AND {$p}_userJobHistory.job IN (:{$p}_jobs)
DQL,
UserHistory::class,
UserJobHistory::class,
),
)
)
->setParameter("{$p}_jobs", $data['jobs'])
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
;
}
@ -95,20 +89,29 @@ class UserJobFilter implements FilterInterface
'expanded' => true,
'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
'label' => 'Job',
]);
])
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_job.Start from',
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_job.Until',
])
;
}
public function describeAction($data, $format = 'string')
{
return [
'export.filter.course.by_user_job.Filtered by user job: only %job%', [
'%job%' => implode(
'exports.filter.course.by_user_job.Filtered by user job: only job', [
'job' => implode(
', ',
array_map(
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
)
),
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
],
];
}
@ -117,9 +120,30 @@ class UserJobFilter implements FilterInterface
{
return [
'jobs' => [],
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
if (null === $before) {
return $default;
}
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
$before['start_date'] = $default['start_date'];
}
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
$before['end_date'] = $default['end_date'];
}
return $before;
}
public function getTitle(): string
{
return 'export.filter.course.by_user_job.Filter by user job';

View File

@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
class UserScopeFilter implements FilterInterface
final readonly class UserScopeFilter implements FilterInterface, DataTransformerInterface
{
private const PREFIX = 'acp_filter_main_scope';
public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelper $translatableStringHelper,
private ScopeRepositoryInterface $scopeRepository,
private TranslatableStringHelper $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
@ -37,47 +42,35 @@ class UserScopeFilter implements FilterInterface
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
public function alterQuery(QueryBuilder $qb, $data): void
{
$p = self::PREFIX;
$qb
->join(
'acp.userHistories',
"{$p}_userHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
)
)
$qb->andWhere(
$qb->expr()->exists(
sprintf(
<<<DQL
SELECT 1
FROM %s {$p}_userHistory
JOIN %s {$p}_userScopeHistory
WITH
{$p}_userHistory.user = {$p}_userScopeHistory.user
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userScopeHistory.startDate, {$p}_userScopeHistory.endDate) = TRUE
WHERE {$p}_userHistory.accompanyingPeriod = acp
AND {$p}_userHistory.startDate <= :{$p}_endDate
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
AND {$p}_userScopeHistory.startDate <= :{$p}_endDate
AND ({$p}_userScopeHistory.endDate IS NULL OR {$p}_userScopeHistory.endDate > :{$p}_startDate)
AND {$p}_userScopeHistory.scope IN (:{$p}_scopes)
DQL,
UserHistory::class,
UserScopeHistory::class,
),
)
->join(
UserScopeHistory::class,
"{$p}_scopeHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_scopeHistory.user", "{$p}_userHistory.user"),
$qb->expr()->andX(
$qb->expr()->lte("{$p}_scopeHistory.startDate", "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes"))
->setParameter(
"{$p}_scopes",
$data['scopes'],
)
;
)
->setParameter("{$p}_scopes", $data['scopes'])
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
}
public function applyOn(): string
@ -94,20 +87,28 @@ class UserScopeFilter implements FilterInterface
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
'multiple' => true,
'expanded' => true,
])
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_scope.Start from',
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_scope.Until',
]);
}
public function describeAction($data, $format = 'string')
{
return [
'export.filter.course.by_user_scope.Filtered by user main scope: only %scope%', [
'%scope%' => implode(
'exports.filter.course.by_user_scope.Filtered by user main scope: only scopes', [
'scopes' => implode(
', ',
array_map(
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
$data['scopes'] instanceof Collection ? $data['scopes']->toArray() : $data['scopes']
)
),
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
],
];
}
@ -116,9 +117,30 @@ class UserScopeFilter implements FilterInterface
{
return [
'scopes' => [],
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
if (null === $before) {
return $default;
}
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
$before['start_date'] = $default['start_date'];
}
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
$before['end_date'] = $default['end_date'];
}
return $before;
}
public function getTitle(): string
{
return 'export.filter.course.by_user_scope.Filter by user scope';

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\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
final readonly class PersonQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private AuthorizationCheckerInterface $authorizationChecker) {}
public static function getMenuIds(): array
{
return ['person_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\Person $person */
$person = $parameters['person'];
if ($this->authorizationChecker->isGranted(AccompanyingPeriodVoter::CREATE, $person)) {
$menu->addChild(
'Create Accompanying Course',
[
'route' => 'chill_person_accompanying_course_new',
'routeParameters' => [
'person_id' => [$person->getId()],
],
]
)
->setExtras([
'order' => 10,
'icon' => 'plus',
]);
}
}
}

View File

@ -30,7 +30,7 @@ div.list-with-period {
// override wrap-list
div.wrap-list.periods-list {
padding-right: 1rem;
padding-right: 0;
div.wl-row {
flex-wrap: nowrap;
div.wl-col {

View File

@ -1,6 +1,6 @@
<template>
<a class="btn" :class="getClassButton" :title="$t(buttonTitle)" @click="openModal">
<span v-if="displayTextButton">{{ $t(buttonTitle) }}</span>
<a class="btn" :class="getClassButton" :title="$t(buttonTitle || '')" @click="openModal">
<span v-if="displayTextButton">{{ $t(buttonTitle || '') }}</span>
</a>
<teleport to="body">

View File

@ -7,10 +7,8 @@
{% endif %}
<a id="comment-{{ comment.id }}" href="{{ '#comment-' ~ comment.id }}" class="fa fa-pencil-square-o fa-fw"></a>
{% set creator = comment.creator is defined ? comment.creator : comment.createdBy %}
{{ 'by'|trans }}<b>{{ creator }}</b>
{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
{{ 'by'|trans }}
<span class="badge-user">{{ comment.createdBy|chill_entity_render_box({'at_date': comment.createdAt }) }}</span>{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
<i>{{ 'Last updated on'|trans ~ ' ' ~ comment.updatedAt|format_datetime('long', 'short') }}</i>
</div>
<ul class="record_actions">

View File

@ -99,7 +99,7 @@
<div class="metadata">
{{ 'Last updated by'| trans }}
<span class="user">
{{ accompanyingCourse.pinnedComment.updatedBy|chill_entity_render_box }}
{{ accompanyingCourse.pinnedComment.updatedBy|chill_entity_render_box({'at_date': accompanyingCourse.pinnedComment.updatedAt}) }}
</span>
{{ 'on'|trans ~ ' ' }}
<span class="date">

View File

@ -83,9 +83,9 @@
</div>
<div class="wl-col list">
{%- if w.referrers|length > 0 -%}
{% for u in w.referrers %}
{% for r in w.referrersHistory %}
<span class="wl-item">
<span class="badge-user">{{ u|chill_entity_render_box }}</span>
<span class="badge-user">{{ r.user|chill_entity_render_box({'at_date': r.startDate}) }}</span>
{% if not loop.last %}, {% endif %}
</span>
{% endfor %}

View File

@ -33,8 +33,8 @@
{% if w.referrers %}
<li>
<span class="item-key">{{ 'Referrers'|trans ~ ' : ' }}</span>
{% for u in w.referrers %}
<span class="badge-user">{{ u|chill_entity_render_box }}</span>
{% for rh in w.referrersHistory %}
<span class="badge-user">{{ rh.user|chill_entity_render_box({'at_date': rh.startDate}) }}</span>
{% endfor %}
{% if w.referrers|length == 0 %}
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>

View File

@ -89,7 +89,7 @@
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
<b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>

View File

@ -13,7 +13,7 @@
{{ 'Last updated by'|trans }}
{% endif %}
<span class="user">
{{ entity.updatedBy|chill_entity_render_box }}
{{ entity.updatedBy|chill_entity_render_box({'at_date': entity.updatedAt }) }}
</span>
{% endif %}
</div>
@ -34,8 +34,8 @@
{{ 'Created by'|trans }}
{% endif %}
<span class="user">
{{ entity.createdBy|chill_entity_render_string }}
{{ entity.createdBy|chill_entity_render_string({'at_date': entity.createdAt}) }}
</span>
{% endif %}
</div>
{% endmacro %}
{% endmacro %}

View File

@ -1,13 +1,19 @@
{% macro button_person_before(person) %}
{{ chill_menu('person_quick_menu', {
'layout': '@ChillMain/Menu/quick_menu.html.twig',
'args' : { 'person': person }
}) }}
{% endmacro %}
{% macro button_person_after(person) %}
{% set household = person.getCurrentHousehold %}
{% if household is not null and is_granted('CHILL_PERSON_HOUSEHOLD_SEE', household) %}
<li>
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE', person) %}
<li>
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}" class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}"
class="btn btn-sm btn-chill-beige"
title="{{ 'Show household'|trans }}"
><i class="fa fa-home"></i>
</a>
</li>
{% endif %}
{% endmacro %}
@ -165,7 +171,24 @@
<h3>{{ 'chill_calendar.Next calendars'|trans }}</h3>
</div>
<div class="wl-col list">
{% for c in calendars %}<span>{{ c.startDate|format_datetime('long', 'short') }}</span>{% if not loop.last %}, {% endif %}{% endfor %}
<div class="calendar-list">
<ul class="calendar-list">
{% for c in calendars %}
<li>
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {id: c.id}) }}">
<span class="badge bg-secondary">{{ c.startDate|format_datetime('long', 'short') }}</span>
</a>
{% else %}
<span class="badge bg-secondary">{{ c.startDate|format_datetime('long', 'short') }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', acp) %}
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_list_by_period', {'id': acp.id}) }}" class="calendar-list__global"><i class="fa fa-list"></i></a>
{% endif %}
</div>
</div>
</div>
@ -184,13 +207,20 @@
</div>
{% endif %}
<ul class="record_actions record_actions_column">
<ul class="record_actions">
{{ chill_menu('accompanying_course_quick_menu', {
'layout': '@ChillMain/Menu/quick_menu.html.twig',
'args' : { 'accompanying-course': acp }
}) }}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
<i class="fa fa-random fa-fw"></i>
class="btn btn-sm btn-primary"
title="{{ 'See accompanying period'|trans }}"
><i class="fa fa-random fa-fw"></i>
</a>
</li>
</ul>
</div>
</div>
@ -247,7 +277,7 @@
'addAltNames': true,
'addCenter': true,
'address_multiline': false,
'customButtons': { 'after': _self.button_person_after(person) }
'customButtons': { 'after': _self.button_person_after(person), 'before': _self.button_person_before((person)) }
}) }}
{#- 'acps' is for AcCompanyingPeriodS #}

View File

@ -296,7 +296,7 @@ This view should receive those arguments:
<div class="created-updated">
{% if person.createdBy %}
<div class="createdBy">
{{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box }}</b>,<br>
{{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box({'at_date': person.createdAt}) }}</b>,<br>
{{ 'on'|trans ~ person.createdAt|format_datetime('long', 'short') }}
</div>
{% endif %}

View File

@ -64,7 +64,7 @@
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
<b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>

View File

@ -92,7 +92,7 @@
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
<b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
@ -81,6 +82,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
{
if ($period instanceof AccompanyingPeriod) {
$scopes = $this->scopeResolverDispatcher->isConcerned($period) ? $this->scopeResolverDispatcher->resolveScope($period) : [];
$userHistory = $period->getCurrentUserHistory();
if (!\is_array($scopes)) {
$scopes = [$scopes];
@ -101,7 +103,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\ClosingMotive::class])),
'confidential' => $period->isConfidential(),
'createdAt' => $this->normalizer->normalize($period->getCreatedAt(), $format, $dateContext),
'createdBy' => $this->normalizer->normalize($period->getCreatedBy(), $format, $userContext),
'createdBy' => $this->normalizer->normalize($period->getCreatedBy(), $format, [...$userContext, UserNormalizer::AT_DATE => $period->getCreatedAt()]),
'emergency' => $period->isEmergency(),
'openingDate' => $this->normalizer->normalize($period->getOpeningDate(), $format, $dateContext),
'origin' => $this->normalizer->normalize($period->getOrigin(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Origin::class])),
@ -123,7 +125,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'isClosed' => null !== $period->getClosingDate(),
'closingMotiveText' => null !== $period->getClosingMotive() ?
$this->closingMotiveRender->renderString($period->getClosingMotive(), []) : '',
'ref' => $this->normalizer->normalize($period->getUser(), $format, $userContext),
'ref' => $this->normalizer->normalize($userHistory?->getUser(), $format, [...$userContext, UserNormalizer::AT_DATE => $userHistory?->getStartDate()]),
'hasRef' => null !== $period->getUser(),
'socialIssuesText' => implode(', ', array_map(fn (SocialIssue $s) => $this->socialIssueRender->renderString($s, []), $period->getSocialIssues()->toArray())),
'scopesText' => implode(', ', array_map(fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), $scopes)),
@ -135,7 +137,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'locationPerson' => $this->normalizer->normalize($period->getPersonLocation(), $format, array_merge($context, ['docgen:expects' => Person::class])),
'location' => $this->normalizer->normalize($period->getLocation(), $format, $addressContext),
'administrativeLocation' => $this->normalizer->normalize($period->getAdministrativeLocation(), $format, $administrativeLocationContext),
'works' => $this->normalizer->normalize($period->getWorks(), $format, $workContext),
'works' => $this->normalizer->normalize($period->getWorks()->getValues(), $format, $workContext),
'comments' => $this->normalizer->normalize($period->getComments(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])),
'pinnedComment' => $this->normalizer->normalize($period->getPinnedComment(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])),
];

View File

@ -11,12 +11,16 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@ -30,16 +34,48 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
public function __construct(private readonly Registry $registry, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly MetadataExtractor $metadataExtractor) {}
/**
* @param AccompanyingPeriodWork $object
*
* @throws ExceptionInterface
*/
public function normalize($object, ?string $format = null, array $context = []): array|\ArrayObject|bool|float|int|string|null
{
if (!$object instanceof AccompanyingPeriodWork && 'json' === $format) {
throw new UnexpectedValueException('Object cannot be null or empty when format is json');
}
if ('docgen' === $format && !($object instanceof AccompanyingPeriodWork || null === $object)) {
throw new UnexpectedValueException(sprintf('Object must be an instanceof AccompanyingPeriodWork or null when format is docgen, %s given', get_debug_type($object)));
}
$cleanContext = array_filter($context, fn (string|int $key) => !in_array($key, ['docgen:expects', self::IGNORE_WORK], true), ARRAY_FILTER_USE_KEY);
if (null === $object && 'docgen' === $format) {
$dateNull = $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => \DateTimeImmutable::class]);
$userNull = $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => User::class]);
return [
'isNull' => true,
'type' => 'accompanying_period_work',
'accompanyingPeriodWorkEvaluations' => [],
'referrers' => [],
'createdAt' => $dateNull,
'createdAutomatically' => 'false',
'createdAutomaticallyReason' => '',
'createdBy' => $userNull,
'endDate' => $dateNull,
'goals' => [],
'handlingThierParty' => $this->normalizer->normalize(null, $format, [...$cleanContext, 'docgen:expects' => ThirdParty::class]),
'id' => '',
'note' => '',
'persons' => [],
'results' => [],
'socialAction' => $this->normalizer->normalize(null, $format, [...$cleanContext, 'docgen:expects' => SocialAction::class]),
'startDate' => $dateNull,
'thirdParties' => [],
'updatedAt' => $dateNull,
'updatedBy' => $userNull,
];
}
$initial = $this->normalizer->normalize($object, $format, array_merge(
$context,
[self::IGNORE_WORK => spl_object_hash($object)]
$cleanContext,
[self::IGNORE_WORK => null === $object ? null : spl_object_hash($object)]
));
// due to bug: https://api-platform.com/docs/core/serialization/#collection-relation
@ -48,38 +84,57 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
$initial['accompanyingPeriodWorkEvaluations'] = $this->normalizer->normalize(
$object->getAccompanyingPeriodWorkEvaluations()->getValues(),
$format,
$context
[...$cleanContext]
);
// then, we add normalization for things which are not into the entity
// add the referrers
$initial['referrers'] = [];
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWork::class,
$object->getId()
);
foreach ($object->getReferrersHistory() as $referrerHistory) {
if (null !== $referrerHistory->getEndDate()) {
continue;
}
$initial['workflows_availables_evaluation'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluation::class
);
$initial['referrers'][] = $this->normalizer->normalize(
$referrerHistory->getUser(),
$format,
[...$cleanContext, UserNormalizer::AT_DATE => $referrerHistory->getStartDate()]
);
}
$initial['workflows_availables_evaluation_documents'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluationDocument::class
);
if ('json' === $format) {
// then, we add normalization for things which are not into the entity
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWork::class,
$object->getId()
);
$workflows = $this->entityWorkflowRepository->findBy([
'relatedEntityClass' => AccompanyingPeriodWork::class,
'relatedEntityId' => $object->getId(),
]);
$initial['workflows_availables_evaluation'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluation::class
);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context);
$initial['workflows_availables_evaluation_documents'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluationDocument::class
);
$workflows = $this->entityWorkflowRepository->findBy([
'relatedEntityClass' => AccompanyingPeriodWork::class,
'relatedEntityId' => $object->getId(),
]);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context);
}
return $initial;
}
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
return 'json' === $format
&& $data instanceof AccompanyingPeriodWork
&& !\array_key_exists(self::IGNORE_WORK, $context);
return match ($format) {
'json' => $data instanceof AccompanyingPeriodWork && ($context[self::IGNORE_WORK] ?? null) !== spl_object_hash($data),
'docgen' => ($data instanceof AccompanyingPeriodWork || (null === $data && ($context['docgen:expects'] ?? null) === AccompanyingPeriodWork::class))
&& !array_key_exists(self::IGNORE_WORK, $context),
default => false,
};
}
}

View File

@ -48,7 +48,7 @@ class SocialActionNormalizer implements NormalizerAwareInterface, NormalizerInte
'type' => 'social_work_social_action',
'text' => $this->render->renderString($socialAction, []),
'title' => $socialAction->getTitle(),
'parent' => $this->normalizer->normalize($socialAction->getParent(), $format, $context),
'parent' => $this->normalizer->normalize($socialAction->getParent(), $format, [...$context, 'docgen:expects' => SocialAction::class]),
'issue' => $this->normalizer->normalize($socialAction->getIssue(), $format, $context),
];

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
@ -46,7 +47,7 @@ class WorkflowNormalizer implements ContextAwareNormalizerInterface, NormalizerA
$data['workflow'] = $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow);
$data['current_place'] = $this->metadataExtractor->buildArrayPresentationForPlace($object);
$data['current_place_at'] = $this->normalizer->normalize($object->getCurrentStepCreatedAt(), 'json', ['groups' => ['read']]);
$data['current_place_by'] = $this->normalizer->normalize($object->getCurrentStepCreatedBy(), 'json', ['groups' => ['read']]);
$data['current_place_by'] = $this->normalizer->normalize($object->getCurrentStepCreatedBy(), 'json', ['groups' => ['read'], UserNormalizer::AT_DATE => $object->getCurrentStepCreatedAt()]);
return $data;
}

View File

@ -33,7 +33,40 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest
$this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer');
}
public function getAggregator()
/**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
['date_calc' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
null,
// this is the default configuration
['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}
public function getAggregator(): ReferrerAggregator
{
return $this->aggregator;
}
@ -41,7 +74,10 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest
public static function getFormData(): array
{
return [
['date_calc' => new RollingDate(RollingDate::T_TODAY)],
[
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
],
];
}

View File

@ -14,6 +14,7 @@ namespace Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
@ -48,16 +49,46 @@ final class ReferrerScopeAggregatorTest extends AbstractAggregatorTest
return new ReferrerScopeAggregator(
$scopeRepository->reveal(),
$translatableStringHelper->reveal(),
$dateConverter->reveal()
new RollingDateConverter(),
);
}
public static function getFormData(): array
{
return [
[
'date_calc' => new RollingDate(RollingDate::T_TODAY),
],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}
/**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
null,
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
[],
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}

View File

@ -33,7 +33,39 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest
$this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer_job');
}
public function getAggregator()
/**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
null,
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
[],
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}
public function getAggregator(): UserJobAggregator
{
return $this->aggregator;
}
@ -41,9 +73,7 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest
public static function getFormData(): array
{
return [
[
'job_at' => new RollingDate(RollingDate::T_FIXED_DATE, \DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01')),
],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}

View File

@ -50,11 +50,13 @@ final class UserJobFilterTest extends AbstractFilterTest
$data = [];
$data[] = [
'jobs' => new ArrayCollection($jobs),
'date_calc' => new RollingDate(RollingDate::T_TODAY),
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
$data[] = [
'jobs' => $jobs,
'date_calc' => new RollingDate(RollingDate::T_TODAY),
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
return $data;

View File

@ -50,11 +50,13 @@ final class UserScopeFilterTest extends AbstractFilterTest
return [
[
'date_calc' => new RollingDate(RollingDate::T_TODAY),
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
'scopes' => new ArrayCollection($scopes),
],
[
'date_calc' => new RollingDate(RollingDate::T_TODAY),
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
'scopes' => $scopes,
],
];

View File

@ -11,13 +11,13 @@ declare(strict_types=1);
namespace Serializer\Normalizer;
use Chill\DocGeneratorBundle\Test\DocGenNormalizerTestAbstract;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -25,8 +25,10 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
* @internal
*
* @coversNothing
*
* @template-extends DocGenNormalizerTestAbstract<AccompanyingPeriodWork>
*/
final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
final class AccompanyingPeriodWorkDocGenNormalizerTest extends DocGenNormalizerTestAbstract
{
private NormalizerInterface $normalizer;
@ -36,28 +38,31 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
$this->normalizer = self::getContainer()->get(NormalizerInterface::class);
}
public function testNormalizationNull()
public function provideNotNullObject(): object
{
$actual = $this->normalizer->normalize(null, 'docgen', [
'docgen:expects' => AccompanyingPeriodWork::class,
AbstractNormalizer::GROUPS => ['docgen:read'],
]);
$work = new AccompanyingPeriodWork();
$work
->addPerson((new Person())->setFirstName('hello')->setLastName('name'))
->addGoal($g = new AccompanyingPeriodWorkGoal())
->addResult($r = new Result())
->setCreatedAt(new \DateTimeImmutable())
->setUpdatedAt(new \DateTimeImmutable())
->setCreatedBy($user = new User())
->setUpdatedBy($user);
$g->addResult($r)->setGoal($goal = new Goal());
$goal->addResult($r);
$expected = [
'id' => '',
];
return $work;
}
$this->assertIsArray($actual);
$this->markTestSkipped('specification still not finalized');
$this->assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
public function provideDocGenExpectClass(): string
{
return AccompanyingPeriodWork::class;
}
foreach ($expected as $key => $item) {
if ('@ignored' === $item) {
continue;
}
$this->assertEquals($item, $actual[$key]);
}
public function getNormalizer(): NormalizerInterface
{
return $this->normalizer;
}
public function testNormalize()
@ -79,20 +84,6 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
AbstractNormalizer::GROUPS => ['docgen:read'],
]);
$expected = [
'id' => 0,
];
$this->assertIsArray($actual);
$this->markTestSkipped('specification still not finalized');
$this->assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach ($expected as $key => $item) {
if (0 === $item) {
continue;
}
$this->assertEquals($item, $actual[$key]);
}
}
}

View File

@ -1,19 +1,9 @@
services:
Chill\PersonBundle\Menu\:
resource: './../../Menu'
autowire: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\SectionMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
_defaults:
autowire: true
autoconfigure: true
Chill\PersonBundle\Menu\:
resource: './../../Menu'
tags:
- { name: 'chill.menu_builder' }

View File

@ -143,6 +143,10 @@ exports:
by_referrer_between_dates:
description: >-
Filtré par référent du parcours, entre deux dates: depuis le {start_date, date, medium}, jusqu'au {end_date, date, medium}, seulement {agents}
by_user_job:
"Filtered by user job: only job": "Filtré par métier du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {job}"
by_user_scope:
"Filtered by user main scope: only scopes": "Filtré par service du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {scopes}"
work:
by_treating_agent:
Filtered by treating agent at date: >-

View File

@ -1058,9 +1058,15 @@ export:
by-user:
title: Grouper les parcours par usager participant
header: Usager participant
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
Referrer after: Référent après le
Until: Jusqu'au
by_referrer_scope:
Referrer and scope after: Référent et service après le
Until: Jusqu'au
by_referrer_job:
Referrer and job after: Référent et métier après le
Until: Jusqu'au
by_user_scope:
Group course by referrer's scope: Grouper les parcours par service du référent
Referrer's scope: Service du référent de parcours
@ -1215,7 +1221,8 @@ export:
'Filtered by steps: only %step% and between %date_from% and %date_to%': 'Filtré par statut: seulement %step%, entre %date_from% et %date_to%'
by_user_scope:
Filter by user scope: Filtrer les parcours par service du référent
"Filtered by user main scope: only %scope%": "Filtré par service du référent: uniquement %scope%"
Start from: Référent et service depuis le
Until: Jusqu'au
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
by_referrer_between_dates:
@ -1232,7 +1239,8 @@ export:
'Filtered by creator job: only %jobs%': "Filtré par métier du créateur: uniquement %jobs%"
by_user_job:
Filter by user job: Filtrer les parcours par métier du référent
"Filtered by user job: only %job%": "Filtré par métier du référent: uniquement %job%"
Start from: Référent et métier depuis le
Until: Jusqu'au
by_social_action:
title: Filtrer les parcours par action d'accompagnement
Accepted socialactions: Actions d'accompagnement
@ -1405,7 +1413,8 @@ export:
updatedBy: Modifié par
acp_id: Identifiant du parcours
acp_user: Référent du parcours
referrers: Agents traitants
acpwReferrers: Agents traitants
referrer: Référent du parcours
personsId: Identifiants des usagers
personsName: Usagers de l'action
goalsId: Identifiants des objectifs

View File

@ -85,7 +85,6 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
/**
* [fr] Sigle.
*/
// #[Assert\Length(min: 2)]
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'acronym', type: \Doctrine\DBAL\Types\Types::STRING, length: 64, nullable: true)]
private ?string $acronym = '';
@ -184,7 +183,6 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
/**
* [fr] Raison sociale.
*/
// #[Assert\Length(min: 3)]
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'name_company', type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)]
private ?string $nameCompany = '';

View File

@ -23,7 +23,7 @@ if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env
(new Dotenv(false))->populate($env);
} else {
// load all the .env files
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env.test');
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env');
}
$_SERVER += $_ENV;