Merge branch 'master' into upgrade-sf5

This commit is contained in:
Julien Fastré 2024-06-24 10:46:21 +02:00
commit 916724c0c5
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
99 changed files with 1442 additions and 422 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

@ -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,51 @@
<?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

@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase
$this->assertInstanceOf(
ActivityType::class,
$form->getData()['type'],
'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType'
'The data is an instance of Chill\ActivityBundle\Entity\ActivityType'
);
$this->assertEquals($type->getId(), $form->getData()['type']->getId());

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,50 @@
<?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

@ -37,12 +37,12 @@ class RemoteEventConverter
* valid when the remote string contains also a timezone, like in
* lastModifiedDate.
*/
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P';
/**
* Same as above, but sometimes the date is expressed with only 6 milliseconds.
*/
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP';
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP';
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';

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

@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface
$defaultTransporter = new Reference(NullShortMessageSender::class);
} elseif ('ovh' === $dsn['scheme']) {
if (!class_exists('\\'.\Ovh\Api::class)) {
throw new RuntimeException('Class \\Ovh\\Api not found');
throw new RuntimeException('Class \Ovh\Api not found');
}
foreach (['user', 'host', 'pass'] as $component) {

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

@ -190,7 +190,7 @@ class ExportManager
// throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`');
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
@ -203,7 +203,7 @@ class ExportManager
'dql' => $query->getDQL(),
]);
} else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.');
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);

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

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

@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils;
class ExtractDateFromPattern
{
private const DATE_PATTERN = [
['([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))', 'Y-m-d'], // 1981-05-12
['((0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/([12]\\d{3}))', 'd/m/Y'], // 15/12/1980
['((0[1-9]|[12]\\d|3[01])-(0[1-9]|1[0-2])-([12]\\d{3}))', 'd-m-Y'], // 15/12/1980
['([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))', 'Y-m-d'], // 1981-05-12
['((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))', 'd/m/Y'], // 15/12/1980
['((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))', 'd-m-Y'], // 15/12/1980
];
public function extractDates(string $subject): SearchExtractionResult

View File

@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class ExtractPhonenumberFromPattern
{
private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})';
private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})';
private readonly string $defaultCarrierCode;

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,18 @@ 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 +83,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,32 @@ 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 +66,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,18 @@ 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 +50,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 +70,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,21 +13,27 @@ 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 +52,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 +77,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 +95,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,21 +13,27 @@ 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 +57,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 +76,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 +94,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,24 +13,30 @@ 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 +47,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 +90,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 +121,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,71 +13,65 @@ 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
{
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 +88,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 +118,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,51 @@
<?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

@ -74,7 +74,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase
$this->assertResponseRedirects();
$location = $this->client->getResponse()->headers->get('Location');
$this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location));
$this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location));
}
/**
@ -93,7 +93,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase
$location = $this->client->getResponse()->headers->get('Location');
$matches = [];
$this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location, $matches));
$this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location, $matches));
$id = $matches[1];
$period = self::getContainer()->get(EntityManagerInterface::class)

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

@ -41,7 +41,7 @@ final class ChillReportExtensionTest extends KernelTestCase
}
if (!$reportFounded) {
throw new \Exception('Class Chill\\ReportBundle\\Entity\\Report not found in chill_custom_fields.customizables_entities', 1);
throw new \Exception('Class Chill\ReportBundle\Entity\Report not found in chill_custom_fields.customizables_entities', 1);
}
}
}

View File

@ -32,10 +32,10 @@ return static function (ContainerConfigurator $container) {
->autoconfigure();
$services
->load('Chill\\WopiBundle\\Service\\', __DIR__.'/../../Service');
->load('Chill\WopiBundle\Service\\', __DIR__.'/../../Service');
$services
->load('Chill\\WopiBundle\\Controller\\', __DIR__.'/../../Controller')
->load('Chill\WopiBundle\Controller\\', __DIR__.'/../../Controller')
->tag('controller.service_arguments');
$services