mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-16 11:44:59 +00:00
Compare commits
116 Commits
Author | SHA1 | Date | |
---|---|---|---|
db3961275b
|
|||
cd488d7576 | |||
436661d952 | |||
c3b8d42047 | |||
|
9c28df25a1 | ||
|
c5a24e8ac5 | ||
|
d9c50cffb7 | ||
|
25ccb16308 | ||
|
ba25c181f5 | ||
145419a76b | |||
b1ba5cc608 | |||
87a6757e5e | |||
47f4cfddbb
|
|||
e95f9e9846 | |||
1f4bef754d
|
|||
19e34d5dc0
|
|||
fab00f679c
|
|||
791b3776c5
|
|||
6bd38f1a58
|
|||
68d21c9267
|
|||
e7ca89e0c1
|
|||
fc8bc33ba9
|
|||
cbd9489810
|
|||
90b615c5b2
|
|||
5ca222b501 | |||
3e4495dd6e
|
|||
bca0d04201
|
|||
f66ac50571 | |||
b454774836
|
|||
008f344e49
|
|||
90bfd87ec6
|
|||
cc0030c1cd
|
|||
d60ba3ecb2
|
|||
cd5001ac74 | |||
98f47ac512
|
|||
31b541d12f
|
|||
72045ce082
|
|||
0bfb3de465
|
|||
9ec4c77fb7
|
|||
77c53972c8
|
|||
350d991a85
|
|||
0ce9cdd07a
|
|||
1993fac1c4
|
|||
83883567a2
|
|||
29d57934a1
|
|||
f43d79c940
|
|||
be730679c8
|
|||
f62f1891d8
|
|||
ebb856fe85
|
|||
61877e0157
|
|||
4c3f082163
|
|||
a7ec7c9f37 | |||
c9e13be736 | |||
b9b342fe44 | |||
31f29f0bc5 | |||
0bc9fff825 | |||
25f93e8a89 | |||
4e0d8e4def | |||
1ecc825945 | |||
addc623add | |||
1b96deb4ee | |||
f510acd170 | |||
835409cb94 | |||
2121b3ef28 | |||
6c9101c167 | |||
b46883fe36 | |||
8d58805abd | |||
c3a799cb7d | |||
bc683b28d6 | |||
d91b1a70bf | |||
853014d8d2 | |||
ad6154a1e4 | |||
50c04382ef | |||
d62e9ce269 | |||
2149ef1cb4 | |||
d15fbadd27 | |||
fbbf421d8b | |||
fe695f1a14 | |||
d0ec6f9819 | |||
0b739fda34 | |||
9b8e143855 | |||
a533ab77ed | |||
087032881b | |||
82667a1c0f | |||
db6408926b | |||
f5c7ab6ef0 | |||
a13ada2937 | |||
3be8a39a1a | |||
d7eb1e01da | |||
bd62202d22 | |||
0e3de2ec8a | |||
aa2a398f9e | |||
33187448a0 | |||
a4482ad28b | |||
8ed5a023e8 | |||
653ac1d62b | |||
499009ac43 | |||
192b161e78 | |||
1b1f355123 | |||
39a863448c | |||
0c1a4a5f59 | |||
6f358ee1a9 | |||
0f36b9349b | |||
d18cc29acf | |||
4220d1a2d3 | |||
1ae27152c2 | |||
b946f8c10a | |||
62d6106801 | |||
89fb87f71f | |||
1337360690 | |||
9324c33caf | |||
c2dd9ef676 | |||
a42d7231d9 | |||
38deaf6f36 | |||
04fc5b6614 | |||
384b2be577 |
31
.changes/v2.21.0.md
Normal file
31
.changes/v2.21.0.md
Normal 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,
|
||||
* …
|
||||
|
6
.changes/v2.22.0.md
Normal file
6
.changes/v2.22.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v2.22.0 - 2024-06-25
|
||||
### Feature
|
||||
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
|
||||
|
||||
### Traduction francophone
|
||||
* Exports sont ajoutés pour la module événement.
|
5
.changes/v2.22.1.md
Normal file
5
.changes/v2.22.1.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## v2.22.1 - 2024-07-01
|
||||
### Fixed
|
||||
* Remove debug word
|
||||
### DX
|
||||
* Add a command for reading official address DB from Luxembourg and update chill addresses
|
39
CHANGELOG.md
39
CHANGELOG.md
@@ -6,6 +6,45 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v2.22.0 - 2024-06-25
|
||||
### Feature
|
||||
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
|
||||
|
||||
### Traduction francophone
|
||||
* Exports sont ajoutés pour la module événement.
|
||||
|
||||
## 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
|
||||
|
@@ -76,7 +76,7 @@
|
||||
"phpunit/phpunit": ">= 7.5",
|
||||
"psalm/plugin-phpunit": "^0.18.4",
|
||||
"psalm/plugin-symfony": "^4.0.2",
|
||||
"rector/rector": "^1.1.0",
|
||||
"rector/rector": "1.1.1",
|
||||
"symfony/debug-bundle": "^5.1",
|
||||
"symfony/dotenv": "^4.4",
|
||||
"symfony/maker-bundle": "^1.20",
|
||||
|
@@ -42,11 +42,13 @@
|
||||
"@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": "^3.0.0",
|
||||
"mime": "^4.0.0",
|
||||
"swagger-ui": "^4.15.5",
|
||||
"vis-network": "^9.1.0",
|
||||
"vue": "^3.2.37",
|
||||
|
@@ -32,9 +32,13 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
//define sets of rules
|
||||
$rectorConfig->sets([
|
||||
LevelSetList::UP_TO_PHP_82,
|
||||
\Rector\Symfony\Set\SymfonyLevelSetList::UP_TO_SYMFONY_44,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44,
|
||||
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
|
||||
\Rector\PHPUnit\Set\PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
|
||||
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90,
|
||||
]);
|
||||
|
||||
// some routes are added twice if it remains activated
|
||||
|
@@ -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',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -87,7 +87,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) }}
|
||||
@@ -114,7 +114,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 +142,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 %}
|
||||
|
@@ -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 %}
|
||||
|
||||
|
@@ -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 %}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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());
|
||||
|
||||
|
@@ -222,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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -524,6 +524,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;
|
||||
|
@@ -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',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
||||
|
@@ -1 +1,2 @@
|
||||
import './scss/badge.scss';
|
||||
import './scss/calendar-list.scss';
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
:removableIfSet="false"
|
||||
:displayPicked="false"
|
||||
:suggested="this.suggestedUsers"
|
||||
:label="'Utilisateur principal'"
|
||||
@addNewEntity="setMainUser"
|
||||
></pick-entity>
|
||||
</div>
|
||||
|
@@ -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) %}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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é
|
||||
|
@@ -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;
|
||||
}
|
@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Form\EventType;
|
||||
use Chill\EventBundle\Form\Type\PickEventType;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
@@ -433,7 +433,6 @@ final class EventController extends AbstractController
|
||||
$builder->add('event_id', HiddenType::class, [
|
||||
'data' => $event->getId(),
|
||||
]);
|
||||
dump($event->getId());
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Form\ParticipationType;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
||||
use Chill\EventBundle\Security\ParticipationVoter;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
@@ -11,8 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\DependencyInjection;
|
||||
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\EventBundle\Security\ParticipationVoter;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
@@ -33,12 +33,13 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/authorization.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/forms.yaml');
|
||||
$loader->load('services/repositories.yaml');
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/timeline.yaml');
|
||||
$loader->load('services/export.yaml');
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
|
@@ -0,0 +1,110 @@
|
||||
<?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\EventBundle\Export\Aggregator;
|
||||
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class EventDateAggregator implements AggregatorInterface
|
||||
{
|
||||
private const CHOICES = [
|
||||
'by month' => 'month',
|
||||
'by week' => 'week',
|
||||
'by year' => 'year',
|
||||
];
|
||||
|
||||
private const DEFAULT_CHOICE = 'year';
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$order = null;
|
||||
|
||||
switch ($data['frequency']) {
|
||||
case 'month':
|
||||
$fmt = 'YYYY-MM';
|
||||
|
||||
break;
|
||||
|
||||
case 'week':
|
||||
$fmt = 'YYYY-IW';
|
||||
|
||||
break;
|
||||
|
||||
case 'year':
|
||||
$fmt = 'YYYY';
|
||||
$order = 'DESC';
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \RuntimeException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
|
||||
}
|
||||
|
||||
$qb->addSelect(sprintf("TO_CHAR(event.date, '%s') AS date_aggregator", $fmt));
|
||||
$qb->addGroupBy('date_aggregator');
|
||||
$qb->addOrderBy('date_aggregator', $order);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('frequency', ChoiceType::class, [
|
||||
'choices' => self::CHOICES,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return ['frequency' => self::DEFAULT_CHOICE];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
return static function ($value) use ($data): string {
|
||||
if ('_header' === $value) {
|
||||
return 'by '.$data['frequency'];
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return match ($data['frequency']) {
|
||||
default => $value,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['date_aggregator'];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Group event by date';
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
<?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\EventBundle\Export\Aggregator;
|
||||
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\EventBundle\Repository\EventTypeRepository;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class EventTypeAggregator implements AggregatorInterface
|
||||
{
|
||||
final public const KEY = 'event_type_aggregator';
|
||||
|
||||
public function __construct(protected EventTypeRepository $eventTypeRepository, protected TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
if (!\in_array('eventtype', $qb->getAllAliases(), true)) {
|
||||
$qb->leftJoin('event.type', 'eventtype');
|
||||
}
|
||||
|
||||
$qb->addSelect(sprintf('IDENTITY(event.type) AS %s', self::KEY));
|
||||
$qb->addGroupBy(self::KEY);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// no form required for this aggregator
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data): \Closure
|
||||
{
|
||||
return function (int|string|null $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Event type';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value || null === $t = $this->eventTypeRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translatableStringHelper->localize($t->getName());
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return [self::KEY];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Group by event type';
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
<?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\EventBundle\Export\Aggregator;
|
||||
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\EventBundle\Repository\RoleRepository;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class RoleAggregator implements AggregatorInterface
|
||||
{
|
||||
final public const KEY = 'part_role_aggregator';
|
||||
|
||||
public function __construct(protected RoleRepository $roleRepository, protected TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
if (!\in_array('event_part', $qb->getAllAliases(), true)) {
|
||||
$qb->leftJoin('event_part.role', 'role');
|
||||
}
|
||||
|
||||
$qb->addSelect(sprintf('IDENTITY(event_part.role) AS %s', self::KEY));
|
||||
$qb->addGroupBy(self::KEY);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT_PARTICIPANTS;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// no form required for this aggregator
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data): \Closure
|
||||
{
|
||||
return function (int|string|null $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Participant role';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value || null === $r = $this->roleRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translatableStringHelper->localize($r->getName());
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return [self::KEY];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Group by participant role';
|
||||
}
|
||||
}
|
22
src/Bundle/ChillEventBundle/Export/Declarations.php
Normal file
22
src/Bundle/ChillEventBundle/Export/Declarations.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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\EventBundle\Export;
|
||||
|
||||
/**
|
||||
* This class declare constants used for the export framework.
|
||||
*/
|
||||
abstract class Declarations
|
||||
{
|
||||
final public const EVENT = 'event';
|
||||
|
||||
final public const EVENT_PARTICIPANTS = 'event_participants';
|
||||
}
|
@@ -0,0 +1,127 @@
|
||||
<?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\EventBundle\Export\Export;
|
||||
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\EventBundle\Repository\ParticipationRepository;
|
||||
use Chill\EventBundle\Security\ParticipationVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
|
||||
readonly class CountEventParticipations implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ParticipationRepository $participationRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes()
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return 'Count participants to an event by various parameters.';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of events';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_event_participants' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'Count event participants' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['export_count_event_participants'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Count event participants';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::EVENT_PARTICIPANTS;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->participationRepository
|
||||
->createQueryBuilder('event_part')
|
||||
->join('event_part.person', 'person');
|
||||
|
||||
$qb->select('COUNT(event_part.id) as export_count_event_participants');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ParticipationVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
{
|
||||
return [
|
||||
Declarations::EVENT_PARTICIPANTS,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
128
src/Bundle/ChillEventBundle/Export/Export/CountEvents.php
Normal file
128
src/Bundle/ChillEventBundle/Export/Export/CountEvents.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\EventBundle\Export\Export;
|
||||
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
|
||||
readonly class CountEvents implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private EventRepository $eventRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes()
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return 'Count events by various parameters.';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of events';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_event' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'Number of events' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['export_count_event'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Count events';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::EVENT;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->eventRepository
|
||||
->createQueryBuilder('event')
|
||||
->leftJoin('event.participations', 'epart')
|
||||
->leftJoin('epart.person', 'person');
|
||||
|
||||
$qb->select('COUNT(event.id) as export_count_event');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return EventVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
{
|
||||
return [
|
||||
Declarations::EVENT,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
<?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\EventBundle\Export\Filter;
|
||||
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class EventDateFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(protected TranslatorInterface $translator, private readonly RollingDateConverterInterface $rollingDateConverter)
|
||||
{
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$where = $qb->getDQLPart('where');
|
||||
$clause = $qb->expr()->between(
|
||||
'event.date',
|
||||
':date_from',
|
||||
':date_to'
|
||||
);
|
||||
|
||||
if ($where instanceof Expr\Andx) {
|
||||
$where->add($clause);
|
||||
} else {
|
||||
$where = $qb->expr()->andX($clause);
|
||||
}
|
||||
|
||||
$qb->add('where', $where);
|
||||
$qb->setParameter(
|
||||
'date_from',
|
||||
$this->rollingDateConverter->convert($data['date_from'])
|
||||
);
|
||||
$qb->setParameter(
|
||||
'date_to',
|
||||
$this->rollingDateConverter->convert($data['date_to'])
|
||||
);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder
|
||||
->add('date_from', PickRollingDateType::class, [
|
||||
'label' => 'Events after this date',
|
||||
])
|
||||
->add('date_to', PickRollingDateType::class, [
|
||||
'label' => 'Events before this date',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
return [
|
||||
'Filtered by date of event: only between %date_from% and %date_to%',
|
||||
[
|
||||
'%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
|
||||
'%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Filtered by event date';
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
<?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\EventBundle\Export\Filter;
|
||||
|
||||
use Chill\EventBundle\Entity\EventType;
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\EventBundle\Repository\EventTypeRepository;
|
||||
use Chill\MainBundle\Export\ExportElementValidatedInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class EventTypeFilter implements ExportElementValidatedInterface, FilterInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatableStringHelperInterface $translatableStringHelper,
|
||||
protected EventTypeRepository $eventTypeRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$clause = $qb->expr()->in('event.type', ':selected_event_types');
|
||||
|
||||
$qb->andWhere($clause);
|
||||
$qb->setParameter('selected_event_types', $data['types']);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('types', EntityType::class, [
|
||||
'choices' => $this->eventTypeRepository->findAllActive(),
|
||||
'class' => EventType::class,
|
||||
'choice_label' => fn (EventType $ety) => $this->translatableStringHelper->localize($ety->getName()),
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'attr' => [
|
||||
'class' => 'select2',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
$typeNames = array_map(
|
||||
fn (EventType $t): string => $this->translatableStringHelper->localize($t->getName()),
|
||||
$this->eventTypeRepository->findBy(['id' => $data['types'] instanceof \Doctrine\Common\Collections\Collection ? $data['types']->toArray() : $data['types']])
|
||||
);
|
||||
|
||||
return ['Filtered by event type: only %list%', [
|
||||
'%list%' => implode(', ', $typeNames),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Filtered by event type';
|
||||
}
|
||||
|
||||
public function validateForm($data, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === $data['types'] || 0 === \count($data['types'])) {
|
||||
$context
|
||||
->buildViolation('At least one type must be chosen')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
95
src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php
Normal file
95
src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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\EventBundle\Export\Filter;
|
||||
|
||||
use Chill\EventBundle\Entity\Role;
|
||||
use Chill\EventBundle\Export\Declarations;
|
||||
use Chill\EventBundle\Repository\RoleRepository;
|
||||
use Chill\MainBundle\Export\ExportElementValidatedInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class RoleFilter implements ExportElementValidatedInterface, FilterInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatableStringHelperInterface $translatableStringHelper,
|
||||
protected RoleRepository $roleRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$clause = $qb->expr()->in('event_part.role', ':selected_part_roles');
|
||||
|
||||
$qb->andWhere($clause);
|
||||
$qb->setParameter('selected_part_roles', $data['part_roles']);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::EVENT_PARTICIPANTS;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('part_roles', EntityType::class, [
|
||||
'choices' => $this->roleRepository->findAllActive(),
|
||||
'class' => Role::class,
|
||||
'choice_label' => fn (Role $r) => $this->translatableStringHelper->localize($r->getName()),
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'attr' => [
|
||||
'class' => 'select2',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
$roleNames = array_map(
|
||||
fn (Role $r): string => $this->translatableStringHelper->localize($r->getName()),
|
||||
$this->roleRepository->findBy(['id' => $data['part_roles'] instanceof \Doctrine\Common\Collections\Collection ? $data['part_roles']->toArray() : $data['part_roles']])
|
||||
);
|
||||
|
||||
return ['Filtered by participant roles: only %list%', [
|
||||
'%list%' => implode(', ', $roleNames),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'Filter by participant roles';
|
||||
}
|
||||
|
||||
public function validateForm($data, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === $data['part_roles'] || 0 === \count($data['part_roles'])) {
|
||||
$context
|
||||
->buildViolation('At least one role must be chosen')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Menu;
|
||||
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Menu;
|
||||
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
@@ -13,7 +13,7 @@ namespace Chill\EventBundle\Repository;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
|
@@ -12,13 +12,57 @@ declare(strict_types=1);
|
||||
namespace Chill\EventBundle\Repository;
|
||||
|
||||
use Chill\EventBundle\Entity\Role;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class RoleRepository extends ServiceEntityRepository
|
||||
readonly class RoleRepository implements ObjectRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, private TranslatableStringHelper $translatableStringHelper)
|
||||
{
|
||||
parent::__construct($registry, Role::class);
|
||||
$this->repository = $entityManager->getRepository(Role::class);
|
||||
}
|
||||
|
||||
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
|
||||
{
|
||||
return $this->repository->createQueryBuilder($alias, $indexBy);
|
||||
}
|
||||
|
||||
public function find($id)
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findAllActive(): array
|
||||
{
|
||||
$roles = $this->repository->findBy(['active' => true]);
|
||||
|
||||
usort($roles, fn (Role $a, Role $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName()));
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria)
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return Role::class;
|
||||
}
|
||||
}
|
||||
|
@@ -9,18 +9,19 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\EventBundle\Security\Authorization;
|
||||
namespace Chill\EventBundle\Security;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
|
||||
/**
|
||||
* Description of EventVoter.
|
||||
@@ -42,61 +43,46 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
|
||||
|
||||
final public const UPDATE = 'CHILL_EVENT_UPDATE';
|
||||
|
||||
/**
|
||||
* @var AccessDecisionManagerInterface
|
||||
*/
|
||||
protected $accessDecisionManager;
|
||||
final public const STATS = 'CHILL_EVENT_STATS';
|
||||
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
private readonly VoterHelperInterface $voterHelper;
|
||||
|
||||
public function __construct(
|
||||
AccessDecisionManagerInterface $accessDecisionManager,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
LoggerInterface $logger
|
||||
private readonly AuthorizationHelper $authorizationHelper,
|
||||
private readonly LoggerInterface $logger,
|
||||
VoterHelperFactoryInterface $voterHelperFactory
|
||||
) {
|
||||
$this->accessDecisionManager = $accessDecisionManager;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->logger = $logger;
|
||||
$this->voterHelper = $voterHelperFactory
|
||||
->generate(self::class)
|
||||
->addCheckFor(null, [self::SEE])
|
||||
->addCheckFor(Event::class, [...self::ROLES])
|
||||
->addCheckFor(Person::class, [self::SEE, self::CREATE])
|
||||
->addCheckFor(Center::class, [self::STATS])
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return self::ROLES;
|
||||
return [...self::ROLES, self::STATS];
|
||||
}
|
||||
|
||||
public function getRolesWithHierarchy(): array
|
||||
{
|
||||
return [
|
||||
'Event' => self::ROLES,
|
||||
'Event' => $this->getRoles(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRolesWithoutScope(): array
|
||||
{
|
||||
return [];
|
||||
return [self::ROLES, self::STATS];
|
||||
}
|
||||
|
||||
public function supports($attribute, $subject)
|
||||
{
|
||||
return ($subject instanceof Event && \in_array($attribute, self::ROLES, true))
|
||||
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|
||||
|| (null === $subject && self::SEE === $attribute);
|
||||
return $this->voterHelper->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param Event $subject
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$this->logger->debug(sprintf('Voting from %s class', self::class));
|
||||
@@ -118,15 +104,5 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
|
||||
->getReachableCenters($token->getUser(), $attribute);
|
||||
|
||||
return \count($centers) > 0;
|
||||
|
||||
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->authorizationHelper->userHasAccess(
|
||||
$token->getUser(),
|
||||
$subject,
|
||||
$attribute
|
||||
);
|
||||
}
|
||||
}
|
@@ -9,18 +9,19 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\EventBundle\Security\Authorization;
|
||||
namespace Chill\EventBundle\Security;
|
||||
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
|
||||
class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
@@ -39,40 +40,33 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
|
||||
|
||||
final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE';
|
||||
|
||||
/**
|
||||
* @var AccessDecisionManagerInterface
|
||||
*/
|
||||
protected $accessDecisionManager;
|
||||
final public const STATS = 'CHILL_EVENT_PARTICIPATION_STATS';
|
||||
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
private readonly VoterHelperInterface $voterHelper;
|
||||
|
||||
public function __construct(
|
||||
AccessDecisionManagerInterface $accessDecisionManager,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
LoggerInterface $logger
|
||||
private readonly AuthorizationHelper $authorizationHelper,
|
||||
private readonly LoggerInterface $logger,
|
||||
VoterHelperFactoryInterface $voterHelperFactory
|
||||
) {
|
||||
$this->accessDecisionManager = $accessDecisionManager;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->logger = $logger;
|
||||
$this->voterHelper = $voterHelperFactory
|
||||
->generate(self::class)
|
||||
->addCheckFor(null, [self::SEE])
|
||||
->addCheckFor(Participation::class, [...self::ROLES])
|
||||
->addCheckFor(Person::class, [self::SEE, self::CREATE])
|
||||
->addCheckFor(Center::class, [self::STATS])
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return self::ROLES;
|
||||
return [...self::ROLES, self::STATS];
|
||||
}
|
||||
|
||||
public function getRolesWithHierarchy(): array
|
||||
{
|
||||
return [
|
||||
'Event' => self::ROLES,
|
||||
'Participation' => $this->getRoles(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -83,14 +77,11 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
|
||||
|
||||
public function supports($attribute, $subject)
|
||||
{
|
||||
return ($subject instanceof Participation && \in_array($attribute, self::ROLES, true))
|
||||
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|
||||
|| (null === $subject && self::SEE === $attribute);
|
||||
return $this->voterHelper->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param Participation $subject
|
||||
* @param string $attribute
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -115,15 +106,5 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
|
||||
->getReachableCenters($token->getUser(), $attribute);
|
||||
|
||||
return \count($centers) > 0;
|
||||
|
||||
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->authorizationHelper->userHasAccess(
|
||||
$token->getUser(),
|
||||
$subject,
|
||||
$attribute
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\EventBundle\Tests\Export;
|
||||
|
||||
use Chill\EventBundle\Export\Export\CountEventParticipations;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountEventParticipationsTest extends KernelTestCase
|
||||
{
|
||||
private CountEventParticipations $countEventParticipations;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
$this->countEventParticipations = self::$container->get(CountEventParticipations::class);
|
||||
}
|
||||
|
||||
public function testExecuteQuery(): void
|
||||
{
|
||||
$qb = $this->countEventParticipations->initiateQuery([], [], [])
|
||||
->setMaxResults(1);
|
||||
|
||||
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||
|
||||
self::assertIsArray($results, 'smoke test: test that the result is an array');
|
||||
}
|
||||
}
|
43
src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php
Normal file
43
src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\EventBundle\Tests\Export;
|
||||
|
||||
use Chill\EventBundle\Export\Export\CountEvents;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountEventTest extends KernelTestCase
|
||||
{
|
||||
private CountEvents $countEvents;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
$this->countEvents = self::$container->get(CountEvents::class);
|
||||
}
|
||||
|
||||
public function testExecuteQuery(): void
|
||||
{
|
||||
$qb = $this->countEvents->initiateQuery([], [], [])
|
||||
->setMaxResults(1);
|
||||
|
||||
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||
|
||||
self::assertIsArray($results, 'smoke test: test that the result is an array');
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
<?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 Export\aggregators;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Export\Aggregator\EventDateAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventDateAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private $aggregator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::$container->get(EventDateAggregator::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
{
|
||||
return $this->aggregator;
|
||||
}
|
||||
|
||||
public function getFormData(): array|\Generator
|
||||
{
|
||||
yield ['frequency' => 'YYYY'];
|
||||
yield ['frequency' => 'YYYY-MM'];
|
||||
yield ['frequency' => 'YYYY-IV'];
|
||||
}
|
||||
|
||||
public function getQueryBuilders(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
<?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 Export\aggregators;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Export\Aggregator\EventTypeAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventTypeAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private $aggregator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::$container->get(EventTypeAggregator::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
{
|
||||
return $this->aggregator;
|
||||
}
|
||||
|
||||
public function getFormData(): array
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
<?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 Export\aggregators;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Export\Aggregator\RoleAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class RoleAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private $aggregator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::$container->get(RoleAggregator::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
{
|
||||
return $this->aggregator;
|
||||
}
|
||||
|
||||
public function getFormData(): array
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
$em->createQueryBuilder()
|
||||
->select('event_part')
|
||||
->from(Participation::class, 'event_part'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -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 Export\filters;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Export\Filter\EventDateFilter;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventDateFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private RollingDateConverterInterface $rollingDateConverter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
}
|
||||
|
||||
public function getFilter()
|
||||
{
|
||||
return new EventDateFilter($this->rollingDateConverter);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||
'date_to' => new RollingDate(RollingDate::T_TODAY),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
<?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 Export\filters;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\EventType;
|
||||
use Chill\EventBundle\Export\Filter\EventTypeFilter;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventTypeFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private EventTypeFilter $filter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->filter = self::$container->get(EventTypeFilter::class);
|
||||
}
|
||||
|
||||
public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
$array = $em->createQueryBuilder()
|
||||
->from(EventType::class, 'et')
|
||||
->select('et')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($array as $a) {
|
||||
$data[] = [
|
||||
'types' => new ArrayCollection([$a]),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
<?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 Export\filters;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Entity\Role;
|
||||
use Chill\EventBundle\Export\Filter\RoleFilter;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class RoleFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private RoleFilter $filter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->filter = self::$container->get(RoleFilter::class);
|
||||
}
|
||||
|
||||
public function getFilter()
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
public function getFormData(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
$array = $em->createQueryBuilder()
|
||||
->from(Role::class, 'r')
|
||||
->select('r')
|
||||
->getQuery()
|
||||
->setMaxResults(1)
|
||||
->getResult();
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($array as $a) {
|
||||
$data[] = [
|
||||
'roles' => new ArrayCollection([$a]),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('event.id')
|
||||
->from(Event::class, 'event'),
|
||||
$em->createQueryBuilder()
|
||||
->select('event_part')
|
||||
->from(Participation::class, 'event_part'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\EventBundle\Tests\Repository;
|
||||
|
||||
use Chill\EventBundle\Repository\EventACLAwareRepository;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
@@ -1,20 +0,0 @@
|
||||
services:
|
||||
chill_event.event_voter:
|
||||
class: Chill\EventBundle\Security\Authorization\EventVoter
|
||||
arguments:
|
||||
- "@security.access.decision_manager"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- "@logger"
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
- { name: security.voter }
|
||||
|
||||
chill_event.event_participation:
|
||||
class: Chill\EventBundle\Security\Authorization\ParticipationVoter
|
||||
arguments:
|
||||
- "@security.access.decision_manager"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- "@logger"
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
- { name: security.voter }
|
41
src/Bundle/ChillEventBundle/config/services/export.yaml
Normal file
41
src/Bundle/ChillEventBundle/config/services/export.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
# indicators
|
||||
|
||||
Chill\EventBundle\Export\Export\CountEvents:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_events' }
|
||||
Chill\EventBundle\Export\Export\CountEventParticipations:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_event_participants' }
|
||||
|
||||
# filters
|
||||
|
||||
Chill\EventBundle\Export\Filter\EventDateFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'event_date_filter' }
|
||||
|
||||
Chill\EventBundle\Export\Filter\EventTypeFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'event_type_filter' }
|
||||
|
||||
Chill\EventBundle\Export\Filter\RoleFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'role_filter' }
|
||||
|
||||
# aggregators
|
||||
|
||||
Chill\EventBundle\Export\Aggregator\EventTypeAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: event_type_aggregator }
|
||||
|
||||
Chill\EventBundle\Export\Aggregator\EventDateAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: event_date_aggregator }
|
||||
|
||||
Chill\EventBundle\Export\Aggregator\RoleAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: role_aggregator }
|
14
src/Bundle/ChillEventBundle/config/services/security.yaml
Normal file
14
src/Bundle/ChillEventBundle/config/services/security.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
Chill\EventBundle\Security\EventVoter:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\EventBundle\Security\ParticipationVoter:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
@@ -81,9 +81,31 @@ Pick an event: Choisir un événement
|
||||
Pick a type of event: Choisir un type d'événement
|
||||
Pick a moderator: Choisir un animateur
|
||||
|
||||
# exports
|
||||
Select a format: Choisir un format
|
||||
Export: Exporter
|
||||
|
||||
Count events: Nombre d'événements
|
||||
Count events by various parameters.: Compte le nombre d'événements selon divers critères
|
||||
Exports of events: Exports d'événements
|
||||
|
||||
Filtered by event date: Filtrer par date d'événement
|
||||
'Filtered by date of event: only between %date_from% and %date_to%': "Filtré par date d'événement: uniquement entre le %date_from% et le %date_to%"
|
||||
Events after this date: Événements après cette date
|
||||
Events before this date: Événements avant cette date
|
||||
Filtered by event type: Filtrer par type d'événement
|
||||
'Filtered by event type: only %list%': "Filtré par type: uniquement %list%"
|
||||
Group event by date: Grouper par date d'événement
|
||||
Group by event type: Grouper par type d'événement
|
||||
|
||||
Count event participants: Nombre de participations
|
||||
Count participants to an event by various parameters.: Compte le nombre de participations selon divers critères
|
||||
Exports of event participants: Exports de participations
|
||||
'Filtered by participant roles: only %list%': "Filtré par rôles de participation: uniquement %list%"
|
||||
Filter by participant roles: Filtrer par rôles de participation
|
||||
Part roles: Rôles de participation
|
||||
Group by participant role: Grouper par rôle de participation
|
||||
|
||||
|
||||
Events configuration: Configuration des événements
|
||||
Events configuration menu: Menu des événements
|
||||
|
@@ -0,0 +1,40 @@
|
||||
<?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\Command;
|
||||
|
||||
use Chill\MainBundle\Service\Import\AddressReferenceLU;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LoadAddressesLUFromBDAddressCommand extends Command
|
||||
{
|
||||
protected static $defaultDescription = 'Import LUX addresses from BD addresses (see https://data.public.lu/fr/datasets/adresses-georeferencees-bd-adresses/)';
|
||||
|
||||
public function __construct(
|
||||
private readonly AddressReferenceLU $addressImporter,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('chill:main:address-ref-lux');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->addressImporter->import();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -274,13 +274,13 @@ class User implements UserInterface, \Stringable
|
||||
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();
|
||||
}
|
||||
@@ -326,13 +326,13 @@ class User implements UserInterface, \Stringable
|
||||
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();
|
||||
}
|
||||
@@ -346,6 +346,11 @@ class User implements UserInterface, \Stringable
|
||||
return $this->jobHistories;
|
||||
}
|
||||
|
||||
public function getUserScopeHistories(): Collection
|
||||
{
|
||||
return $this->scopeHistories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|UserJobHistory[]
|
||||
*/
|
||||
|
@@ -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;
|
||||
}
|
@@ -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]);
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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,11 +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']);
|
||||
@@ -34,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')
|
||||
|
@@ -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;
|
||||
@@ -43,6 +45,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);
|
||||
}
|
||||
|
||||
|
@@ -43,7 +43,14 @@ var 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 @@ var 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");
|
||||
|
||||
|
@@ -139,8 +139,8 @@ const postprocess = (html: string): string => {
|
||||
}
|
||||
|
||||
const convertMarkdownToHtml = (markdown: string): string => {
|
||||
marked.use({'hooks': {postprocess, preprocess}});
|
||||
const rawHtml = marked(markdown);
|
||||
marked.use({'hooks': {postprocess, preprocess}, 'async': false});
|
||||
const rawHtml = marked(markdown) as string;
|
||||
return rawHtml;
|
||||
};
|
||||
|
||||
|
@@ -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 }}
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
@@ -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 %}
|
||||
|
@@ -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 %}
|
||||
|
@@ -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 }} :</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 %}
|
||||
|
@@ -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 }} : </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 }} : </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 }} :</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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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.
|
||||
*
|
||||
@@ -86,9 +82,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface
|
||||
{
|
||||
return 'chill_menu';
|
||||
}
|
||||
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -54,7 +54,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(),
|
||||
|
@@ -47,7 +47,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,
|
||||
];
|
||||
|
@@ -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,12 +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,
|
||||
@@ -74,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(),
|
||||
];
|
||||
|
||||
|
@@ -0,0 +1,97 @@
|
||||
<?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\Service\Import;
|
||||
|
||||
use League\Csv\Reader;
|
||||
use League\Csv\Statement;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class AddressReferenceLU
|
||||
{
|
||||
private const RELEASE = 'https://data.public.lu/fr/datasets/r/5cadc5b8-6a7d-4283-87bc-f9e58dd771f7';
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $addressBaseImporter, private readonly PostalCodeBaseImporter $postalCodeBaseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function import(): void
|
||||
{
|
||||
$downloadUrl = self::RELEASE;
|
||||
|
||||
$response = $this->client->request('GET', $downloadUrl);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new \Exception('Could not download CSV: '.$response->getStatusCode());
|
||||
}
|
||||
|
||||
$file = tmpfile();
|
||||
|
||||
foreach ($this->client->stream($response) as $chunk) {
|
||||
fwrite($file, $chunk->getContent());
|
||||
}
|
||||
|
||||
fseek($file, 0);
|
||||
|
||||
$csv = Reader::createFromStream($file);
|
||||
$csv->setDelimiter(';');
|
||||
$csv->setHeaderOffset(0);
|
||||
|
||||
$this->process_postal_code($csv);
|
||||
|
||||
$this->process_address($csv);
|
||||
|
||||
$this->addressToReferenceMatcher->checkAddressesMatchingReferences();
|
||||
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
private function process_address(Reader $csv): void
|
||||
{
|
||||
$stmt = Statement::create()->process($csv);
|
||||
foreach ($stmt as $record) {
|
||||
$this->addressBaseImporter->importAddress(
|
||||
$record['id_geoportail'],
|
||||
$record['code_postal'],
|
||||
$record['code_postal'],
|
||||
$record['rue'],
|
||||
$record['numero'],
|
||||
'bd-addresses.lux',
|
||||
(float) $record['lat_wgs84'],
|
||||
(float) $record['lon_wgs84'],
|
||||
4326
|
||||
);
|
||||
}
|
||||
|
||||
$this->addressBaseImporter->finalize();
|
||||
}
|
||||
|
||||
private function process_postal_code(Reader $csv): void
|
||||
{
|
||||
$stmt = Statement::create()->process($csv);
|
||||
$arr_postal_codes = [];
|
||||
foreach ($stmt as $record) {
|
||||
if (false === \array_key_exists($record['code_postal'], $arr_postal_codes)) {
|
||||
$this->postalCodeBaseImporter->importCode(
|
||||
'LU',
|
||||
trim((string) $record['localite']),
|
||||
trim((string) $record['code_postal']),
|
||||
trim((string) $record['code_postal']),
|
||||
'bd-addresses.lux',
|
||||
(float) $record['lat_wgs84'],
|
||||
(float) $record['lon_wgs84'],
|
||||
4326
|
||||
);
|
||||
$arr_postal_codes[$record['code_postal']] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,17 +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,
|
||||
@@ -45,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']) {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 = [])
|
||||
{
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -59,6 +59,12 @@ services:
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\LoadAddressesLUFromBDAddressCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\ExecuteCronJobCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
@@ -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 }
|
||||
|
@@ -869,19 +869,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1507,6 +1512,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)) {
|
||||
|
@@ -60,13 +60,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
||||
* orphanRemoval=true
|
||||
* )
|
||||
*
|
||||
* @Serializer\Groups({"read", "docgen:read"})
|
||||
* @Serializer\Groups({"read"})
|
||||
*
|
||||
* @ORM\OrderBy({"startDate": "DESC", "id": "DESC"})
|
||||
*
|
||||
* @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`
|
||||
*/
|
||||
private Collection $accompanyingPeriodWorkEvaluations;
|
||||
|
||||
@@ -395,21 +396,21 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
||||
/**
|
||||
* @return ReadableCollection<int, User>
|
||||
*
|
||||
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
|
||||
* @Serializer\Groups({"accompanying_period_work:edit"})
|
||||
* @Serializer\Groups({"accompanying_period_work:create"})
|
||||
*/
|
||||
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;
|
||||
@@ -577,9 +578,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;
|
||||
}
|
||||
@@ -621,14 +622,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;
|
||||
}
|
||||
|
@@ -167,6 +167,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->getCreator();
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
|
@@ -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,14 +22,17 @@ use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class ReferrerAggregator implements AggregatorInterface
|
||||
final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface
|
||||
{
|
||||
private const A = 'acp_ref_agg_uhistory';
|
||||
|
||||
private const P = 'acp_ref_agg_date';
|
||||
|
||||
public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter)
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private UserRender $userRender,
|
||||
private RollingDateConverterInterface $rollingDateConverter
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
@@ -46,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
|
||||
@@ -68,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)
|
||||
|
@@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
readonly class ReferrerScopeAggregator implements AggregatorInterface
|
||||
readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface
|
||||
{
|
||||
private const PREFIX = 'acp_agg_referrer_scope';
|
||||
|
||||
public function __construct(
|
||||
private ScopeRepositoryInterface $scopeRepository,
|
||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -47,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"
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -67,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");
|
||||
}
|
||||
@@ -81,11 +97,34 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
|
||||
|
||||
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)
|
||||
|
@@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\UserJobRepository;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class UserJobAggregator implements AggregatorInterface
|
||||
final readonly class UserJobAggregator implements AggregatorInterface, DataTransformerInterface
|
||||
{
|
||||
private const PREFIX = 'acp_agg_user_job';
|
||||
|
||||
public function __construct(
|
||||
private UserJobRepository $jobRepository,
|
||||
private TranslatableStringHelper $translatableStringHelper
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -52,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"
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -67,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");
|
||||
}
|
||||
@@ -81,11 +96,34 @@ final readonly class UserJobAggregator implements AggregatorInterface
|
||||
|
||||
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)
|
||||
|
@@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserJobFilter implements FilterInterface
|
||||
final readonly class UserJobFilter implements FilterInterface, DataTransformerInterface
|
||||
{
|
||||
private const PREFIX = 'acp_filter_user_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository,
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private UserJobRepositoryInterface $userJobRepository,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -42,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']))
|
||||
;
|
||||
}
|
||||
|
||||
@@ -96,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']),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -118,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';
|
||||
|
@@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserScopeFilter implements FilterInterface
|
||||
final readonly class UserScopeFilter implements FilterInterface, DataTransformerInterface
|
||||
{
|
||||
private const PREFIX = 'acp_filter_main_scope';
|
||||
|
||||
public function __construct(
|
||||
private readonly ScopeRepositoryInterface $scopeRepository,
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private ScopeRepositoryInterface $scopeRepository,
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -38,47 +43,35 @@ class UserScopeFilter implements FilterInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
public function alterQuery(QueryBuilder $qb, $data): void
|
||||
{
|
||||
$p = self::PREFIX;
|
||||
|
||||
$qb
|
||||
->join(
|
||||
'acp.userHistories',
|
||||
"{$p}_userHistory",
|
||||
Join::WITH,
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
||||
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
||||
)
|
||||
)
|
||||
)
|
||||
$qb->andWhere(
|
||||
$qb->expr()->exists(
|
||||
sprintf(
|
||||
<<<DQL
|
||||
SELECT 1
|
||||
FROM %s {$p}_userHistory
|
||||
JOIN %s {$p}_userScopeHistory
|
||||
WITH
|
||||
{$p}_userHistory.user = {$p}_userScopeHistory.user
|
||||
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userScopeHistory.startDate, {$p}_userScopeHistory.endDate) = TRUE
|
||||
WHERE {$p}_userHistory.accompanyingPeriod = acp
|
||||
AND {$p}_userHistory.startDate <= :{$p}_endDate
|
||||
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
|
||||
AND {$p}_userScopeHistory.startDate <= :{$p}_endDate
|
||||
AND ({$p}_userScopeHistory.endDate IS NULL OR {$p}_userScopeHistory.endDate > :{$p}_startDate)
|
||||
AND {$p}_userScopeHistory.scope IN (:{$p}_scopes)
|
||||
DQL,
|
||||
UserHistory::class,
|
||||
UserScopeHistory::class,
|
||||
),
|
||||
)
|
||||
->join(
|
||||
UserScopeHistory::class,
|
||||
"{$p}_scopeHistory",
|
||||
Join::WITH,
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq("{$p}_scopeHistory.user", "{$p}_userHistory.user"),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte("{$p}_scopeHistory.startDate", "{$p}_userHistory.startDate"),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
|
||||
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes"))
|
||||
->setParameter(
|
||||
"{$p}_scopes",
|
||||
$data['scopes'],
|
||||
)
|
||||
;
|
||||
)
|
||||
->setParameter("{$p}_scopes", $data['scopes'])
|
||||
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
@@ -95,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']),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -117,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';
|
||||
|
51
src/Bundle/ChillPersonBundle/Menu/PersonQuickMenuBuilder.php
Normal file
51
src/Bundle/ChillPersonBundle/Menu/PersonQuickMenuBuilder.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user