Compare commits

...

72 Commits

Author SHA1 Message Date
d1a4891b9d upgrade phpunit config 2023-06-05 17:58:52 +02:00
5749660760 Merge branch '108-fix-acc-period-transition-notification' into 'master'
Fixed: Do not send a confirmation message when the accompanying period is back to CONFIRM state

Closes #108

See merge request Chill-Projet/chill-bundles!549
2023-05-25 13:30:15 +00:00
b679dbe26c Fixed: Do not send a confirmation message when period is mark_active back 2023-05-25 15:24:14 +02:00
6c3fa5cb98 Fixed: [UI] in designation and redispatch list, the period's statuses
were'nt shown correctly
2023-05-25 15:22:52 +02:00
6bdd1f31d3 Merge branch '98-rewrite-download-button' into 'master'
Fixed: vue downloadButton: add more log and various improvements

See merge request Chill-Projet/chill-bundles!547
2023-05-25 10:42:10 +00:00
ff3dab0934 Fixed: vue downloadButton: add more log and various improvements
- create a dedicated button for opening
- use nextTick before clicking on the "opening" button
2023-05-25 12:36:47 +02:00
977299192f Merge branch 'deselect-checkbox-exports' into 'master'
Add button to deselect all centers in export

See merge request Chill-Projet/chill-bundles!544
2023-05-24 14:34:57 +00:00
77997e2b6f FIX [review] fix review comments 2023-05-24 16:24:58 +02:00
6e618e688b DX [php-cs-fixer] 2023-05-24 15:50:25 +02:00
dad36927f3 FEATURE [export] uncheck all centers button 2023-05-24 15:34:39 +02:00
f5448f9d95 DX [phpstan] fixes 2023-05-24 14:02:49 +02:00
a31c4063a1 DX [cs-fixer] fixes 2023-05-24 14:02:05 +02:00
c8f95528c0 Merge branch '98-fix-download-document' into 'master'
Resolve "Dans certaines situations, le téléchargement et déchiffrement des documents ne fonctionne pas"

See merge request Chill-Projet/chill-bundles!541
2023-05-24 11:40:03 +00:00
20023dff67 DX: fix cs 2023-05-24 13:35:35 +02:00
d82a3e0ff6 Fixed: [download document] add a target when downloading document 2023-05-24 13:13:57 +02:00
04359f27c6 Revert "Revert "FIX [duplicates] reinstate gestion des doublons""
This reverts commit 5109490aad.
2023-05-24 13:11:40 +02:00
5109490aad Revert "FIX [duplicates] reinstate gestion des doublons"
This reverts commit 5351223d44.
2023-05-24 13:09:04 +02:00
8b82e0c535 FIX [rights] user shouldn't be allowed to see accompanyingperiods from within household 2023-05-23 18:19:31 +02:00
5351223d44 FIX [duplicates] reinstate gestion des doublons 2023-05-23 18:18:44 +02:00
674e057f67 fixes for user permission list 2023-05-20 00:54:35 +02:00
b2e79b677b Feature: [admin] add an export list of all permissions associated to
each user
2023-05-20 00:34:53 +02:00
748e566c7e Feature: [admin] list of users on csv format 2023-05-20 00:03:25 +02:00
1f4c51f3bc Merge branch '96-worflow-regression' into 'master'
Workflow regression in accompanying period work

See merge request Chill-Projet/chill-bundles!538
2023-05-19 10:17:58 +00:00
dc6eeccaab Merge branch 'permission-page/modernize' into 'master'
DX et UX: permissions page

See merge request Chill-Projet/chill-bundles!530
2023-05-19 10:03:08 +00:00
0083842509 DX: modernize controller for permissions groups and order them alphabetically 2023-05-19 11:58:11 +02:00
ca7be4ecd0 Merge branch 'export/allow-check-multiple-geographical-zones' into 'master'
Feature: [export] allow to check multiple geographical layers in geographical aggregators

See merge request Chill-Projet/chill-bundles!532
2023-05-19 09:54:25 +00:00
785c70fe92 Merge branch 'exports/filters-on-work-date' into 'master'
Feature: [exports] add filters for start and end date for accompanying period works

See merge request Chill-Projet/chill-bundles!533
2023-05-19 09:51:06 +00:00
8bbca7e61a Merge branch 'master' into export/allow-check-multiple-geographical-zones 2023-05-19 11:50:12 +02:00
5d21612c2e Merge branch '98-fix-download-document' into 'master'
Fix: [document download] better memory management and introduce delay

See merge request Chill-Projet/chill-bundles!539
2023-05-19 09:45:57 +00:00
fb9f182edd Merge branch 'docgen-add-household-composition' into 'master'
Feature: add Household composition on household in docgen

See merge request Chill-Projet/chill-bundles!537
2023-05-19 09:39:56 +00:00
bbd3d2a83f Fix: [document download] better memory management and introduce delay
before opening

Related to https://gitlab.com/Chill-Projet/chill-bundles/-/issues/98
2023-05-19 11:34:25 +02:00
e87420dc57 Merge branch '20-finalisation-cire' into 'master'
Finalisation cire

See merge request Chill-Projet/chill-bundles!529
2023-05-19 08:26:39 +00:00
f1bf02d2b4 css classes 2023-05-17 19:38:48 +02:00
8a35c2e2ee Fix workflow regression with accompanying period work (introduced by commit 6b90a7d2a7 24/01/2023) 2023-05-17 16:33:07 +02:00
fbd555e89a Feature: add Household composition on household in docgen 2023-05-17 16:05:53 +02:00
66dc027354 Fixed: fix first execution of accompanying period step change cronjob 2023-05-17 13:27:20 +02:00
8863e0a92e Fixed: force string on username 2023-05-17 13:24:44 +02:00
db9fef095a Fixed: force default values for cc users in workflow 2023-05-17 13:24:15 +02:00
1df2342c49 Merge branch 'master' into 20-finalisation-cire 2023-05-17 10:40:01 +02:00
addbdacee8 Merge branch 'feature/change-parcours-status' into 'master'
Feature/change parcours status

See merge request Chill-Projet/chill-bundles!527
2023-05-17 08:13:18 +00:00
8a684734e7 Fixed: fix docgen normalization on household with "old" members
When a household had old members, the indexes of each "current" members
should be numerical and contiguous, to be transformed in a list. If this
is not the case, the members are mapped to an associative array.

This commit alter the generic DocGenObjectNormalizer to ensure that
the ReadableCollection are normalized using the
CollectionDocGenNormalizer as default, which do not preserve keys.
2023-05-16 23:30:02 +02:00
1abaf2acb0 DX: add help and description to use ImportSocialWorkMetadata command 2023-05-10 10:28:36 +02:00
a0ae1f0d0f FIX [budget] display budget element comment if it is of kind 'autre' 2023-05-03 12:11:00 +02:00
2554da9dd8 Fix: urgent fix for EntityToJsonTransformer
The throw on error flag imposes us to propose a valid json string for decoding
2023-04-28 23:19:23 +02:00
3e3f20993d fix migration after test on real data 2023-04-28 15:55:41 +02:00
997f3cdb09 Feature: [exports] add filters for start and end date for accompanying period works 2023-04-28 12:37:28 +02:00
ab5ad7ae14 fix cs 2023-04-28 12:02:31 +02:00
36413f16c3 DX: convert closure to arrow function 2023-04-28 11:51:16 +02:00
f75b90cb26 Feature: [export] add filters regarding to accompanying period infos 2023-04-28 11:49:37 +02:00
1956836f88 add migration to fix existing period steps 2023-04-28 11:49:37 +02:00
ea4294d12d add start date's accompanying period in accompanyingperiod info 2023-04-28 11:49:36 +02:00
c73e57ad73 doc: add info about syncrhonizing views 2023-04-28 11:49:36 +02:00
5b729e1cb1 add dev docs 2023-04-28 11:49:36 +02:00
229af2e4f9 Feature: Adapt filters and aggregators with new steps 2023-04-28 11:49:36 +02:00
e80a6e417b Feature: Track update of entities if no user is associated to the request
This happens in scripts/cli (messenger, ...)
2023-04-28 11:49:35 +02:00
c5989de120 Feature: Adapt UI to show new steps 2023-04-28 11:49:35 +02:00
fcbc00d0f1 Feature: force to add updatedAt and createdAt even if no user iss associated 2023-04-28 11:49:35 +02:00
722f053f06 Feature: Change accompanying period info step in a cronjob 2023-04-28 11:49:35 +02:00
97b7ff2e43 Feature: takes activity into account for AccompanyingPeriodInfo 2023-04-28 11:49:34 +02:00
f3e0302f3f Feature: takes document and evaluation update into account for AccompanyingPeriodInfo 2023-04-28 11:49:34 +02:00
4974995ea2 Feature: add evaluation info to accompangyin preiod info 2023-04-28 11:49:34 +02:00
f2e1c73f37 Build parts to track info on accompanying period 2023-04-28 11:49:33 +02:00
cdaca533a0 Feature: [export] allow to check multiple geographical layers in
geographical aggregators

BC: saved exports which activated this layers won't work any more. This
is the query to find them:

```sql
SELECT * FROM chill_main_saved_export
WHERE
    options->'export'->'export'->'aggregators'->'accompanyingcourse_geographicalunitstat_aggregator'->>'enabled' = '1'
```
2023-04-25 17:37:27 +02:00
73c0dd0e9e Merge branch 'fix-event-bundle' into 20-finalisation-cire 2023-04-24 17:24:44 +02:00
8fd9010ea5 fix event bundle stuffs
- adapt event templates
- event bundle: fix deprecated deps injections
- fix error with n=0 not iterated into querybuilder with centers loop
2023-04-24 17:22:01 +02:00
488a0e5f0c Merge branch 'improve-budget-template' into 20-finalisation-cire 2023-04-24 16:22:30 +02:00
cb0ff88318 Fix action column width 2023-04-24 13:29:45 +02:00
be965e8698 - improve title hierarchy and ergonomie 2023-04-24 13:01:58 +02:00
241e605ea6 - style of h3 subtitle 2023-04-24 12:22:41 +02:00
fe3d437096 improve title hierarchy coherence 2023-04-24 12:12:10 +02:00
c1f5f02c41 Merge branch 'master' into improve-budget-template 2023-04-24 11:51:58 +02:00
087ada2250 UX: Better use of flex-table and tables in budget twig templates 2023-03-30 00:07:38 +02:00
105 changed files with 3211 additions and 364 deletions

View File

@@ -34,6 +34,7 @@
"sensio/framework-extra-bundle": "^5.5",
"spomky-labs/base64url": "^2.0",
"symfony/browser-kit": "^4.4",
"symfony/clock": "^6.2",
"symfony/css-selector": "^4.4",
"symfony/expression-language": "^4.4",
"symfony/form": "^4.4",

View File

@@ -0,0 +1,203 @@
.. Copyright (C) 2014 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
.. _entity-info:
Stats about event on entity in php world
########################################
It is necessary to be able to gather information about events for some entities:
- when the event has been done;
- who did it;
- ...
Those "infos" are not linked with right management, like describe in :ref:`timelines`.
“infos” for some stats and info about an entity
-----------------------------------------------
Building an info means:
- create an Entity, and map this entity to a SQL view (not a regular table);
- use the framework to build this entity dynamically.
A framework api is built to be able to build multiple “infos” entities
through “union” views:
- use a command ``bin/console chill:db:sync-views`` to synchronize view (create view if it does not exists, or update
views when new SQL parts are added in the UNION query. Internally, this command call a new ``ViewEntityInfoManager``,
which iterate over available views to build the SQL;
- one can create a new “view entity info” by implementing a
``ViewEntityInfoProviderInterface``
- this implementation of the interface is free to create another
interface for building each part of the UNION query. This interface
is created for AccompanyingPeriodInfo:
``Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface``
So, converting new “events” into rows for ``AccompanyingPeriodInfo`` is
just implementing this interface!
Implementation for AccompanyingPeriod (``AccompanyingPeriod/AccompanyingPeriodInfo``)
-------------------------------------------------------------------------------------
A class is created for computing some statistical info for an
AccompanyingPeriod: ``AccompanyingPeriod/AccompanyingPeriodInfo``. This
contains information about “something happens”, who did it and when.
Having those info in table answer some questions like:
- when is the last and the first action (AccompanyingPeriodWork,
Activity, AccompanyingPeriodWorkEvaluation, …) on the period;
- who is “acting” on the period, and when is the last “action” for each
user.
The AccompanyingPeriod info is mapped to a SQL view, not a table. The
sql view is built dynamically (see below), and gather infos from
ActivityBundle, PersonBundle, CalendarBundle, … It is possible to create
custom bundle and add info on this view.
.. code:: php
/**
*
* @ORM\Entity()
* @ORM\Table(name="view_chill_person_accompanying_period_info") <==== THIS IS A VIEW, NOT A TABLE
*/
class AccompanyingPeriodInfo
{
// ...
}
Why do we need this ?
~~~~~~~~~~~~~~~~~~~~~
For multiple jobs in PHP world:
- moving the accompanying period to another steps when inactive,
automatically;
- listing all the users which are intervening on the action on a new
“Liste des intervenants” page;
- filtering on exports
Later, we will launch automatic anonymise for accompanying period and
all related entities through this information.
How is built the SQL views which is mapped to “info” entities ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The AccompanyingPeriodInfo entity is mapped by a SQL view (not a regular
table).
The sql view is built dynamically, it is a SQL view like this, for now (April 2023):
.. code:: sql
create view view_chill_person_accompanying_period_info
(accompanyingperiod_id, relatedentity, relatedentityid, user_id, infodate, discriminator, metadata) as
SELECT w.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork'::text AS relatedentity,
w.id AS relatedentityid,
cpapwr.user_id,
w.enddate AS infodate,
'accompanying_period_work_end'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON w.id = cpapwr.accompanyingperiodwork_id
WHERE w.enddate IS NOT NULL
UNION
SELECT cpapw.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation'::text AS relatedentity,
e.id AS relatedentityid,
e.updatedby_id AS user_id,
e.updatedat AS infodate,
'accompanying_period_work_evaluation_updated_at'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
WHERE e.updatedat IS NOT NULL
UNION
SELECT cpapw.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation'::text AS relatedentity,
e.id AS relatedentityid,
cpapwr.user_id,
e.maxdate AS infodate,
'accompanying_period_work_evaluation_start'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id
WHERE e.maxdate IS NOT NULL
UNION
SELECT cpapw.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation'::text AS relatedentity,
e.id AS relatedentityid,
cpapwr.user_id,
e.startdate AS infodate,
'accompanying_period_work_evaluation_start'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id
UNION
SELECT cpapw.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument'::text AS relatedentity,
doc.id AS relatedentityid,
doc.updatedby_id AS user_id,
doc.updatedat AS infodate,
'accompanying_period_work_evaluation_document_updated_at'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work_evaluation_document doc
JOIN chill_person_accompanying_period_work_evaluation e ON doc.accompanyingperiodworkevaluation_id = e.id
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
WHERE doc.updatedat IS NOT NULL
UNION
SELECT cpapw.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation'::text AS relatedentity,
e.id AS relatedentityid,
cpapwr.user_id,
e.maxdate AS infodate,
'accompanying_period_work_evaluation_max'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id
WHERE e.maxdate IS NOT NULL
UNION
SELECT w.accompanyingperiod_id,
'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork'::text AS relatedentity,
w.id AS relatedentityid,
cpapwr.user_id,
w.startdate AS infodate,
'accompanying_period_work_start'::text AS discriminator,
'{}'::jsonb AS metadata
FROM chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON w.id = cpapwr.accompanyingperiodwork_id
UNION
SELECT activity.accompanyingperiod_id,
'Chill\ActivityBundle\Entity\Activity'::text AS relatedentity,
activity.id AS relatedentityid,
au.user_id,
activity.date AS infodate,
'activity_date'::text AS discriminator,
'{}'::jsonb AS metadata
FROM activity
LEFT JOIN activity_user au ON activity.id = au.activity_id
WHERE activity.accompanyingperiod_id IS NOT NULL;
As you can see, the view gather multiple SELECT queries and bind them
with UNION.
Each SELECT query is built dynamically, through a class implementing an
interface: ``Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface``, `like
here <https://gitlab.com/Chill-Projet/chill-bundles/-/blob/master/src/Bundle/ChillPersonBundle/Service/EntityInfo/AccompanyingPeriodInfoQueryPart/AccompanyingPeriodWorkEndQueryPartForAccompanyingPeriodInfo.php>`__
To add new `SELECT` query in different `UNION` parts in the sql view, create a
service and implements this interface: ``Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface``.

View File

@@ -35,6 +35,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
manual/index.rst
Assets <assets.rst>
Cron Jobs <cronjob.rst>
Info about entities <entity-info.rst>
Layout and UI
**************

View File

@@ -6,6 +6,8 @@
A copy of the license is included in the section entitled "GNU
Free Documentation License".
.. _timelines:
Timelines
*********

View File

@@ -151,6 +151,7 @@ This script will :
# mount into to container
./docker-php.sh
bin/console chill:db:sync-views
# and load fixtures
bin/console doctrine:migrations:migrate
@@ -161,7 +162,7 @@ Chill will be available at ``http://localhost:8001.`` Currently, there isn't any
# mount into to container
./docker-php.sh
# and load fixtures
# and load fixtures (do not this for production)
bin/console doctrine:fixtures:load --purge-with-truncate
There are several users available:
@@ -204,8 +205,10 @@ How to create the database schema (= run migrations) ?
# if a container is running
./docker-php.sh
bin/console doctrine:migrations:migrate
bin/console chill:db:sync-views
# if not
docker-compose run --user $(id -u) php bin/console doctrine:migrations:migrate
docker-compose run --user $(id -u) php bin/console chill:db:sync-views
How to read the email sent by the program ?
@@ -236,6 +239,23 @@ How to open a terminal in the project
# if not
docker-compose run --user $(id -u) php /bin/bash
How to run cron-jobs ?
======================
Some command must be executed in :ref:`cron jobs <cronjob>`. To execute them:
.. code-block:: bash
# if a container is running
./docker-php.sh
bin/console chill:cron-job:execute
# some of them are executed only during the night. So, we have to force the execution during the day:
bin/console chill:cron-job:execute 'name-of-the-cron'
# if not
docker-compose run --user $(id -u) php bin/console chill:cron-job:execute
# some of them are executed only during the night. So, we have to force the execution during the day:
docker-compose run --user $(id -u) php bin/console chill:cron-job:execute 'name-of-the-cron'
How to run composer ?
=====================

View File

@@ -38,6 +38,14 @@ This should be adapted to your needs:
* Think about how you will backup your database. Some adminsys find easier to store database outside of docker, which might be easier to administrate or replicate.
Run migrations on each update
=============================
Every time you start a new version, you should apply update the sql schema:
- running ``bin/console doctrine:migration:migrate`` to run sql migration;
- synchonizing sql views to the last state: ``bin/console chill:db:sync-views``
Cron jobs
=========

View File

@@ -2,11 +2,20 @@
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="tests/app/vendor/phpunit/phpunit/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/app/tests/bootstrap.php"
>
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<php>
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />

View File

@@ -650,8 +650,8 @@ final class ActivityController extends AbstractController
throw $this->createNotFoundException('Accompanying Period not found');
}
// TODO Add permission
// $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
// TODO Add permission
// $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
} else {
throw $this->createNotFoundException('Person or Accompanying Period not found');
}

View File

@@ -26,12 +26,12 @@ final class PersonMenuBuilder implements LocalMenuBuilderInterface
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
private $authorizationChecker;
/**
* @var TranslatorInterface
*/
protected $translator;
private $translator;
public function __construct(
AuthorizationCheckerInterface $authorizationChecker,

View File

@@ -0,0 +1,64 @@
<?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\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class ActivityUsersDateQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'activity.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return Activity::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'activity.id';
}
public function getUserIdColumn(): string
{
return 'au.user_id';
}
public function getDateTimeColumn(): string
{
return 'activity.date';
}
public function getDiscriminator(): string
{
return 'activity_date';
}
public function getMetadataColumn(): string
{
return '\'{}\'::jsonb';
}
public function getFromStatement(): string
{
return 'activity
LEFT JOIN activity_user au on activity.id = au.activity_id';
}
public function getWhereClause(): string
{
return 'activity.accompanyingperiod_id IS NOT NULL';
}
}

View File

@@ -34,6 +34,7 @@ services:
resource: '../Validator/Constraints/'
Chill\ActivityBundle\Service\DocGenerator\:
autowire: true
autoconfigure: true
resource: '../Service/DocGenerator/'
Chill\ActivityBundle\Service\EntityInfo\:
resource: '../Service/EntityInfo/'

View File

@@ -16,8 +16,14 @@
<td class="el-type">
{% if f.isResource %}
{{ f.resource.name|localize_translatable_string }}
{% if f.resource.getKind is same as 'other' %}
: {{ f.getComment }}
{% endif %}
{% else %}
{{ f.charge.name|localize_translatable_string }}
{% if f.charge.getKind is same as 'other' %}
: {{ f.getComment }}
{% endif %}
{% endif %}
</td>
<td>{{ f.amount|format_currency('EUR') }}</td>

View File

@@ -13,6 +13,7 @@ namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use ArrayObject;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@@ -51,7 +52,9 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
return false;
}
return $data instanceof Collection
|| (null === $data && Collection::class === ($context['docgen:expects'] ?? null));
return $data instanceof ReadableCollection
|| (null === $data && Collection::class === ($context['docgen:expects'] ?? null))
|| (null === $data && ReadableCollection::class === ($context['docgen:expects'] ?? null))
;
}
}

View File

@@ -13,6 +13,7 @@ namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\Common\Collections\ReadableCollection;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -271,6 +272,14 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
if ($isTranslatable) {
$data[$key] = $this->translatableStringHelper
->localize($value);
} elseif ($value instanceof ReadableCollection) {
// when normalizing collection, we should not preserve keys (to ensure that the result is a list)
// this is why we make call to the normalizer again to use the CollectionDocGenNormalizer
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$objectContext,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (is_iterable($value)) {
$arr = [];

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
class CollectionDocGenNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalizeFilteredArray(): void
{
$coll = new ArrayCollection([
(object) ['v' => 'foo'],
(object) ['v' => 'bar'],
(object) ['v' => 'baz'],
]);
//filter to get non continuous indexes
$criteria = new Criteria();
$criteria->where(Criteria::expr()->neq('v', 'bar'));
$filtered = $coll->matching($criteria);
$normalized = $this->normalizer->normalize($filtered, 'docgen', []);
self::assertIsArray($normalized);
self::assertArrayHasKey(0, $normalized);
self::assertArrayHasKey(1, $normalized);
}
}

View File

@@ -1,51 +1,90 @@
<template>
<a :class="props.classes" @click="download_and_open($event)">
<i class="fa fa-download"></i>
Télécharger
</a>
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
<i class="fa fa-download"></i>
Télécharger
</a>
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
<i class="fa fa-external-link"></i>
Ouvrir
</a>
</template>
<script lang="ts" setup>
import {reactive} from "vue";
import {reactive, ref, nextTick, onMounted} from "vue";
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
import mime from "mime";
import {StoredObject} from "../../types";
interface DownloadButtonConfig {
storedObject: StoredObject,
classes: {[k: string]: boolean},
filename?: string,
storedObject: StoredObject,
classes: { [k: string]: boolean },
filename?: string,
}
interface DownloadButtonState {
content: null|string
is_ready: boolean,
is_running: boolean,
href_url: string,
}
const props = defineProps<DownloadButtonConfig>();
const state: DownloadButtonState = reactive({content: null});
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
const document_name = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
return document_name + '.' + ext;
}
return document_name;
}
async function download_and_open(event: Event): Promise<void> {
const button = event.target as HTMLAnchorElement;
const button = event.target as HTMLAnchorElement;
if (null === state.content) {
event.preventDefault();
if (state.is_running) {
console.log('state is running, aborting');
return;
}
state.is_running = true;
if (state.is_ready) {
console.log('state is ready. This should not happens');
return;
}
const urlInfo = build_download_info_link(props.storedObject.filename);
let raw;
const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
state.content = window.URL.createObjectURL(raw);
button.href = window.URL.createObjectURL(raw);
button.type = props.storedObject.type;
button.download = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
button.download = button.download + '.' + ext;
try {
raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
} catch (e) {
console.error("error while downloading and decrypting document");
console.error(e);
throw e;
}
}
button.click();
console.log('document downloading (and decrypting) successfully');
console.log('creating the url')
state.href_url = window.URL.createObjectURL(raw);
console.log('url created', state.href_url);
state.is_running = false;
state.is_ready = true;
console.log('new button marked as ready');
console.log('will click on button');
console.log('openbutton is now', open_button.value);
await nextTick();
console.log('next tick actions');
console.log('openbutton after next tick', open_button.value);
open_button.value?.click();
console.log('open button should have been clicked');
}
</script>

View File

@@ -149,16 +149,21 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe
}
if (iv.length === 0) {
console.log('returning document immediatly');
return rawResponse.blob();
}
console.log('start decrypting doc');
const rawBuffer = await rawResponse.arrayBuffer();
try {
const key = await window.crypto.subtle
.importKey('jwk', keyData, { name: algo }, false, ['decrypt']);
console.log('key created');
const decrypted = await window.crypto.subtle
.decrypt({ name: algo, iv: iv }, key, rawBuffer);
console.log('doc decrypted');
return Promise.resolve(new Blob([decrypted]));
} catch (e) {

View File

@@ -2,11 +2,15 @@
<p>{% transchoice total with { '%pattern%' : pattern } %}%total% events match the search %pattern%{% endtranschoice %}</p>
<style>
table.events td:last-child {
width: 15em;
}
</style>
{% if events|length > 0 %}
<p>{{ 'Results %start%-%end% of %total%'|trans({ '%start%' : start, '%end%': start + events|length, '%total%' : total } ) }}</p>
<table class="table events">
<table class="table table-bordered border-dark align-middle events">
<thead>
<tr>
<th class="chill-red">{{ 'Name'|trans }}</th>
@@ -25,7 +29,7 @@
<ul class="record_actions">
<li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-dark">
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-view">
{{ 'See'|trans }}
</a>
{# {% endif %} #}

View File

@@ -24,7 +24,7 @@
{% block content %}
<h2>{{ 'Events participation' |trans }}</h2>
<table class="table table-striped table-bordered mt-3 events">
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
<thead>
<tr>
<th class="chill-green">{{ 'Date'|trans }}</th>

View File

@@ -18,7 +18,7 @@
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-chill-green' } }) }}
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-submit' } }) }}
</li>
</ul>

View File

@@ -8,7 +8,7 @@
<div class="col-10">
<h1>{{ 'Details of an event'|trans }}</h1>
<table class="table record_properties">
<table class="table table-bordered border-dark align-middle">
<tbody>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -70,7 +70,7 @@
<p>{% transchoice count %}%count% participations to this event{% endtranschoice %}</p>
{% if count > 0 %}
<table class="table">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Person'|trans }}</th>

View File

@@ -12,13 +12,14 @@ declare(strict_types=1);
namespace Chill\EventBundle\Search;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface as TemplatingEngine;
use function count;
@@ -40,15 +41,17 @@ class EventSearch extends AbstractSearch
{
public const NAME = 'event_regular';
private $security;
/**
* @var EntityRepository
* @var EventRepository
*/
private $er;
/**
* @var AuthorizationHelper
*/
private $helper;
private $authorizationHelper;
/**
* @var PaginatorFactory
@@ -60,21 +63,16 @@ class EventSearch extends AbstractSearch
*/
private $templating;
/**
* @var \Chill\MainBundle\Entity\User
*/
private $user;
public function __construct(
TokenStorageInterface $tokenStorage,
EntityRepository $eventRepository,
Security $security,
EventRepository $eventRepository,
AuthorizationHelper $authorizationHelper,
TemplatingEngine $templating,
PaginatorFactory $paginatorFactory
) {
$this->user = $tokenStorage->getToken()->getUser();
$this->security = $security;
$this->er = $eventRepository;
$this->helper = $authorizationHelper;
$this->authorizationHelper = $authorizationHelper;
$this->templating = $templating;
$this->paginationFactory = $paginatorFactory;
}
@@ -101,7 +99,7 @@ class EventSearch extends AbstractSearch
if ('html' === $format) {
return $this->templating->render(
'ChillEventBundle:Event:list.html.twig',
'@ChillEvent/Event/list.html.twig',
[
'events' => $this->search($terms, $start, $limit, $options),
'pattern' => $this->recomposePattern($terms, $this->getAvailableTerms(), $terms['_domain']),
@@ -140,8 +138,10 @@ class EventSearch extends AbstractSearch
protected function composeQuery(QueryBuilder &$qb, $terms)
{
// add security clauses
$reachableCenters = $this->helper
->getReachableCenters($this->user, 'CHILL_EVENT_SEE');
$reachableCenters = $this->authorizationHelper->getReachableCenters(
$this->security->getUser(),
'CHILL_EVENT_SEE'
);
if (count($reachableCenters) === 0) {
// add a clause to block all events
@@ -152,8 +152,9 @@ class EventSearch extends AbstractSearch
$orWhere = $qb->expr()->orX();
foreach ($reachableCenters as $center) {
$circles = $this->helper->getReachableScopes(
$this->user,
$n = $n+1;
$circles = $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
'CHILL_EVENT_SEE',
$center
);

View File

@@ -1,12 +1,11 @@
services:
chill_event.search_events:
class: Chill\EventBundle\Search\EventSearch
Chill\EventBundle\Search\EventSearch:
arguments:
- "@security.token_storage"
- "@chill_event.repository.event"
- "@chill.main.security.authorization.helper"
- "@templating"
- "@chill_main.paginator_factory"
$security: '@Symfony\Component\Security\Core\Security'
$eventRepository: "@chill_event.repository.event"
$authorizationHelper: "@chill.main.security.authorization.helper"
$templating: "@templating"
$paginatorFactory: "@chill_main.paginator_factory"
tags:
- { name: chill.search, alias: 'event_regular' }

View File

@@ -30,7 +30,7 @@ The event was created: L'événement a été créé
#crud participation
Edit all the participations: Modifier toutes les participations
Edit the participation: Modifier la participation
Edit the participation: Modifier la participation à l'événement
Participation Edit: Modifier une participation
Add a participation: Ajouter un participant
Participation creation: Ajouter une participation

View File

@@ -30,6 +30,7 @@ use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Chill\MainBundle\Service\EntityInfo\ViewEntityInfoProviderInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
@@ -62,6 +63,8 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.workflow_handler');
$container->registerForAutoconfiguration(CronJobInterface::class)
->addTag('chill_main.cron_job');
$container->registerForAutoconfiguration(ViewEntityInfoProviderInterface::class)
->addTag('chill_main.entity_info_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

@@ -0,0 +1,39 @@
<?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\EntityInfo\ViewEntityInfoManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SynchronizeEntityInfoViewsCommand extends Command
{
public function __construct(
private ViewEntityInfoManager $viewEntityInfoManager,
) {
parent::__construct('chill:db:sync-views');
}
protected function configure(): void
{
$this
->setDescription('Update or create sql views which provide info for various entities');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->viewEntityInfoManager->synchronizeOnDB();
return 0;
}
}

View File

@@ -16,14 +16,18 @@ use Chill\MainBundle\Entity\RoleScope;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\PermissionsGroupType;
use Chill\MainBundle\Form\Type\ComposedRoleScopeType;
use Chill\MainBundle\Repository\PermissionsGroupRepository;
use Chill\MainBundle\Repository\RoleScopeRepository;
use Chill\MainBundle\Security\RoleProvider;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -32,62 +36,28 @@ use function array_key_exists;
/**
* Class PermissionsGroupController.
*/
class PermissionsGroupController extends AbstractController
final class PermissionsGroupController extends AbstractController
{
/**
* @var RoleHierarchy
*/
private $roleHierarchy;
/**
* @var RoleProvider
*/
private $roleProvider;
/**
* @var TranslatableStringHelper
*/
private $translatableStringHelper;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ValidatorInterface
*/
private $validator;
/**
* PermissionsGroupController constructor.
*/
public function __construct(
TranslatableStringHelper $translatableStringHelper,
RoleProvider $roleProvider,
RoleHierarchy $roleHierarchy,
TranslatorInterface $translator,
ValidatorInterface $validator
private readonly TranslatableStringHelper $translatableStringHelper,
private readonly RoleProvider $roleProvider,
private readonly RoleHierarchyInterface $roleHierarchy,
private readonly TranslatorInterface $translator,
private readonly ValidatorInterface $validator,
private readonly EntityManagerInterface $em,
private readonly PermissionsGroupRepository $permissionsGroupRepository,
private readonly RoleScopeRepository $roleScopeRepository,
) {
$this->translatableStringHelper = $translatableStringHelper;
$this->roleProvider = $roleProvider;
$this->roleHierarchy = $roleHierarchy;
$this->translator = $translator;
$this->validator = $validator;
}
/**
* @param int $id
*
* @throws type
*
* @return Respon
*/
public function addLinkRoleScopeAction(Request $request, $id)
public function addLinkRoleScopeAction(Request $request, int $id): Response
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
$permissionsGroup = $this->permissionsGroupRepository->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -106,7 +76,7 @@ class PermissionsGroupController extends AbstractController
$violations = $this->validator->validate($permissionsGroup);
if ($violations->count() === 0) {
$em->flush();
$this->em->flush();
$this->addFlash(
'notice',
@@ -166,16 +136,15 @@ class PermissionsGroupController extends AbstractController
/**
* Creates a new PermissionsGroup entity.
*/
public function createAction(Request $request)
public function createAction(Request $request): Response
{
$permissionsGroup = new PermissionsGroup();
$form = $this->createCreateForm($permissionsGroup);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($permissionsGroup);
$em->flush();
$this->em->persist($permissionsGroup);
$this->em->flush();
return $this->redirect($this->generateUrl(
'admin_permissionsgroup_edit',
@@ -191,18 +160,11 @@ class PermissionsGroupController extends AbstractController
/**
* remove an association between permissionsGroup and roleScope.
*
* @param int $pgid permissionsGroup id
* @param int $rsid roleScope id
*
* @return redirection to edit form
*/
public function deleteLinkRoleScopeAction($pgid, $rsid)
public function deleteLinkRoleScopeAction(int $pgid, int $rsid): Response
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($pgid);
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)->find($rsid);
$permissionsGroup = $this->permissionsGroupRepository->find($pgid);
$roleScope = $this->roleScopeRepository->find($rsid);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -214,7 +176,7 @@ class PermissionsGroupController extends AbstractController
try {
$permissionsGroup->removeRoleScope($roleScope);
} catch (RuntimeException $ex) {
} catch (RuntimeException) {
$this->addFlash(
'notice',
$this->translator->trans("The role '%role%' and circle "
@@ -231,7 +193,7 @@ class PermissionsGroupController extends AbstractController
));
}
$em->flush();
$this->em->flush();
if ($roleScope->getScope() !== null) {
$this->addFlash(
@@ -260,14 +222,10 @@ class PermissionsGroupController extends AbstractController
/**
* Displays a form to edit an existing PermissionsGroup entity.
*
* @param mixed $id
*/
public function editAction($id)
public function editAction(int $id): Response
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
$permissionsGroup = $this->permissionsGroupRepository->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -311,11 +269,9 @@ class PermissionsGroupController extends AbstractController
/**
* Lists all PermissionsGroup entities.
*/
public function indexAction()
public function indexAction(): Response
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->findAll();
$entities = $this->permissionsGroupRepository->findAllOrderedAlphabetically();
return $this->render('@ChillMain/PermissionsGroup/index.html.twig', [
'entities' => $entities,
@@ -325,7 +281,7 @@ class PermissionsGroupController extends AbstractController
/**
* Displays a form to create a new PermissionsGroup entity.
*/
public function newAction()
public function newAction(): Response
{
$permissionsGroup = new PermissionsGroup();
$form = $this->createCreateForm($permissionsGroup);
@@ -338,14 +294,10 @@ class PermissionsGroupController extends AbstractController
/**
* Finds and displays a PermissionsGroup entity.
*
* @param mixed $id
*/
public function showAction($id)
public function showAction(int $id): Response
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
$permissionsGroup = $this->permissionsGroupRepository->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -393,15 +345,10 @@ class PermissionsGroupController extends AbstractController
/**
* Edits an existing PermissionsGroup entity.
*
* @param mixed $id
*/
public function updateAction(Request $request, $id)
public function updateAction(Request $request, int $id): Response
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em
->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)
$permissionsGroup = $this->permissionsGroupRepository
->find($id);
if (!$permissionsGroup) {
@@ -413,7 +360,7 @@ class PermissionsGroupController extends AbstractController
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->flush();
$this->em->flush();
return $this->redirect($this->generateUrl('admin_permissionsgroup_edit', ['id' => $id]));
}
@@ -452,18 +399,11 @@ class PermissionsGroupController extends AbstractController
/**
* get a role scope by his parameters. The role scope is persisted if it
* doesn't exists in database.
*
* @param Scope $scope
* @param string $role
*
* @return RoleScope
* doesn't exist in database.
*/
protected function getPersistentRoleScopeBy($role, ?Scope $scope = null)
protected function getPersistentRoleScopeBy(string $role, ?Scope $scope = null): RoleScope
{
$em = $this->getDoctrine()->getManager();
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)
$roleScope = $this->roleScopeRepository
->findOneBy(['role' => $role, 'scope' => $scope]);
if (null === $roleScope) {
@@ -471,7 +411,7 @@ class PermissionsGroupController extends AbstractController
->setRole($role)
->setScope($scope);
$em->persist($roleScope);
$this->em->persist($roleScope);
}
return $roleScope;
@@ -479,10 +419,8 @@ class PermissionsGroupController extends AbstractController
/**
* creates a form to add a role scope to permissionsgroup.
*
* @return \Symfony\Component\Form\Form The form
*/
private function createAddRoleScopeForm(PermissionsGroup $permissionsGroup)
private function createAddRoleScopeForm(PermissionsGroup $permissionsGroup): FormInterface
{
return $this->createFormBuilder()
->setAction($this->generateUrl(
@@ -499,10 +437,8 @@ class PermissionsGroupController extends AbstractController
* Creates a form to create a PermissionsGroup entity.
*
* @param PermissionsGroup $permissionsGroup The entity
*
* @return \Symfony\Component\Form\Form The form
*/
private function createCreateForm(PermissionsGroup $permissionsGroup)
private function createCreateForm(PermissionsGroup $permissionsGroup): FormInterface
{
$form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [
'action' => $this->generateUrl('admin_permissionsgroup_create'),
@@ -518,13 +454,11 @@ class PermissionsGroupController extends AbstractController
* Creates a form to delete a link to roleScope.
*
* @param mixed $permissionsGroup The entity id
*
* @return \Symfony\Component\Form\Form The form
*/
private function createDeleteRoleScopeForm(
PermissionsGroup $permissionsGroup,
RoleScope $roleScope
) {
): FormInterface {
return $this->createFormBuilder()
->setAction($this->generateUrl(
'admin_permissionsgroup_delete_role_scope',
@@ -537,12 +471,8 @@ class PermissionsGroupController extends AbstractController
/**
* Creates a form to edit a PermissionsGroup entity.
*
* @param PermissionsGroup $permissionsGroup The entity
*
* @return \Symfony\Component\Form\Form The form
*/
private function createEditForm(PermissionsGroup $permissionsGroup)
private function createEditForm(PermissionsGroup $permissionsGroup): FormInterface
{
$form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [
'action' => $this->generateUrl('admin_permissionsgroup_update', ['id' => $permissionsGroup->getId()]),
@@ -556,10 +486,8 @@ class PermissionsGroupController extends AbstractController
/**
* expand roleScopes to be easily shown in template.
*
* @return array
*/
private function getExpandedRoles(array $roleScopes)
private function getExpandedRoles(array $roleScopes): array
{
$expandedRoles = [];
@@ -567,10 +495,10 @@ class PermissionsGroupController extends AbstractController
if (!array_key_exists($roleScope->getRole(), $expandedRoles)) {
$expandedRoles[$roleScope->getRole()] =
array_map(
static fn (Role $role) => $role->getRole(),
static fn ($role) => $role,
$this->roleHierarchy
->getReachableRoles(
[new Role($roleScope->getRole())]
->getReachableRoleNames(
[$roleScope->getRole()]
)
);
}

View File

@@ -0,0 +1,144 @@
<?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\Controller;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class UserExportController
{
public function __construct(
private UserRepositoryInterface $userRepository,
private Security $security,
private TranslatorInterface $translator,
) {
}
/**
* @throws \League\Csv\CannotInsertRecord
* @throws \League\Csv\Exception
* @throws \League\Csv\UnavailableStream
*
* @Route("/{_locale}/admin/main/users/export/list.{_format}", requirements={"_format": "csv"}, name="chill_main_users_export_list")
*/
public function userList(Request $request, string $_format = 'csv'): StreamedResponse
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list');
}
$users = $this->userRepository->findAllAsArray($request->getLocale());
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.' . $e),
[
'id',
'username',
'email',
'enabled',
'civility_id',
'civility_abbreviation',
'civility_name',
'label',
'mainCenter_id' ,
'mainCenter_name',
'mainScope_id',
'mainScope_name',
'userJob_id',
'userJob_name',
'currentLocation_id',
'currentLocation_name',
'mainLocation_id',
'mainLocation_name',
'absenceStart'
]
)
);
$csv->addFormatter(fn (array $row) => null !== ($row['absenceStart'] ?? null) ? array_merge($row, ['absenceStart' => $row['absenceStart']->format('Y-m-d')]) : $row);
$csv->insertAll($users);
return new StreamedResponse(
function () use ($csv) {
foreach ($csv->chunk(1024) as $chunk) {
echo $chunk;
flush();
}
},
Response::HTTP_OK,
[
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; users.csv',
]
);
}
/**
* @return StreamedResponse
* @throws \League\Csv\CannotInsertRecord
* @throws \League\Csv\Exception
* @throws \League\Csv\UnavailableStream
*
* @Route("/{_locale}/admin/main/users/export/permissions.{_format}", requirements={"_format": "csv"}, name="chill_main_users_export_permissions")
*/
public function userPermissionsList(string $_format = 'csv'): StreamedResponse
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list');
}
$userPermissions = $this->userRepository->findAllUserACLAsArray();
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.' . $e),
[
'id',
'username',
'email',
'label',
'enabled',
'center_id',
'center_name',
'permissionsGroup_id',
'permissionsGroup_name',
]
)
);
$csv->insertAll($userPermissions);
return new StreamedResponse(
function () use ($csv) {
foreach ($csv->chunk(1024) as $chunk) {
echo $chunk;
flush();
}
},
Response::HTTP_OK,
[
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; users.csv',
]
);
}
}

View File

@@ -359,9 +359,9 @@ class WorkflowController extends AbstractController
}
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData();
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData();
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData();
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
$workflow->apply($entityWorkflow, $transition);

View File

@@ -41,12 +41,12 @@ class TrackCreateUpdateSubscriber implements EventSubscriber
{
$object = $args->getObject();
if (
$object instanceof TrackCreationInterface
&& $this->security->getUser() instanceof User
) {
$object->setCreatedBy($this->security->getUser());
if ($object instanceof TrackCreationInterface) {
$object->setCreatedAt(new DateTimeImmutable('now'));
if ($this->security->getUser() instanceof User) {
$object->setCreatedBy($this->security->getUser());
}
}
$this->onUpdate($object);
@@ -61,12 +61,12 @@ class TrackCreateUpdateSubscriber implements EventSubscriber
protected function onUpdate(object $object): void
{
if (
$object instanceof TrackUpdateInterface
&& $this->security->getUser() instanceof User
) {
$object->setUpdatedBy($this->security->getUser());
if ($object instanceof TrackUpdateInterface) {
$object->setUpdatedAt(new DateTimeImmutable('now'));
if ($this->security->getUser() instanceof User) {
$object->setUpdatedBy($this->security->getUser());
}
}
}
}

View File

@@ -506,11 +506,11 @@ class User implements UserInterface
*
* @return User
*/
public function setUsername($name)
public function setUsername(?string $name)
{
$this->username = $name;
$this->username = (string) $name;
if (empty($this->getLabel())) {
if ("" === trim($this->getLabel())) {
$this->setLabel($name);
}

View File

@@ -43,6 +43,10 @@ class EntityToJsonTransformer implements DataTransformerInterface
public function reverseTransform($value)
{
if ("" === $value) {
return null;
}
$denormalized = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
if ($this->multiple) {
@@ -56,10 +60,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
);
}
if ('' === $value) {
return null;
}
return $this->denormalizeOne($denormalized);
}

View File

@@ -38,6 +38,19 @@ final class PermissionsGroupRepository implements ObjectRepository
return $this->repository->findAll();
}
/**
* @return list<PermissionsGroup>
*/
public function findAllOrderedAlphabetically(): array
{
$qb = $this->repository->createQueryBuilder('pg');
return $qb->select(['pg', 'pg.name AS HIDDEN sort_name'])
->orderBy('sort_name')
->getQuery()
->getResult();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
@@ -76,6 +77,81 @@ final class UserRepository implements UserRepositoryInterface
return $this->repository->findAll();
}
/**
* @param string $lang
*/
public function findAllAsArray(string $lang): iterable
{
$dql = sprintf(<<<'DQL'
SELECT
u.id AS id,
u.username AS username,
u.email,
u.enabled,
IDENTITY(u.civility) AS civility_id,
JSON_EXTRACT(civility.abbreviation, :lang) AS civility_abbreviation,
JSON_EXTRACT(civility.name, :lang) AS civility_name,
u.label,
mainCenter.id AS mainCenter_id,
mainCenter.name AS mainCenter_name,
IDENTITY(u.mainScope) AS mainScope_id,
JSON_EXTRACT(mainScope.name, :lang) AS mainScope_name,
IDENTITY(u.userJob) AS userJob_id,
JSON_EXTRACT(userJob.label, :lang) AS userJob_name,
currentLocation.id AS currentLocation_id,
currentLocation.name AS currentLocation_name,
mainLocation.id AS mainLocation_id,
mainLocation.name AS mainLocation_name,
u.absenceStart
FROM Chill\MainBundle\Entity\User u
LEFT JOIN u.civility civility
LEFT JOIN u.currentLocation currentLocation
LEFT JOIN u.mainLocation mainLocation
LEFT JOIN u.mainCenter mainCenter
LEFT JOIN u.mainScope mainScope
LEFT JOIN u.userJob userJob
ORDER BY u.label
DQL);
$query = $this->entityManager->createQuery($dql)
->setHydrationMode(AbstractQuery::HYDRATE_ARRAY)
->setParameter('lang', $lang)
;
foreach ($query->toIterable() as $u) {
yield $u;
}
}
public function findAllUserACLAsArray(): iterable
{
$sql = <<<'SQL'
SELECT
u.id,
u.username,
u.email,
u.label,
u.enabled,
c.id AS center_id,
c.name AS center_name,
pg.id AS permissionsGroup_id,
pg.name AS permissionsGroup_name
FROM users u
LEFT JOIN user_groupcenter ON u.id = user_groupcenter.user_id
LEFT JOIN group_centers ON user_groupcenter.groupcenter_id = group_centers.id
LEFT JOIN centers c on group_centers.center_id = c.id
LEFT JOIN permission_groups pg on group_centers.permissionsgroup_id = pg.id
ORDER BY u.username, c.name, pg.name
SQL;
$query = $this->entityManager->getConnection()->executeQuery($sql);
foreach ($query->iterateAssociative() as $u) {
yield $u;
}
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@@ -14,6 +14,9 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
/**
* @template ObjectRepository<User>
*/
interface UserRepositoryInterface extends ObjectRepository
{
public function countBy(array $criteria): int;
@@ -24,20 +27,25 @@ interface UserRepositoryInterface extends ObjectRepository
public function countByUsernameOrEmail(string $pattern): int;
public function find($id, $lockMode = null, $lockVersion = null): ?User;
/**
* @return User[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
* Find a list of all users.
*
* @return User[]
* The main purpose for this method is to provide a lightweight list of all users in the database.
*
* @param string $lang The lang to display all the translatable string (no fallback if not present)
* @return iterable<array{id: int, username: string, email: string, enabled: bool, civility_id: int, civility_abbreviation: string, civility_name: string, label: string, mainCenter_id: int, mainCenter_name: string, mainScope_id: int, mainScope_name: string, userJob_id: int, userJob_name: string, currentLocation_id: int, currentLocation_name: string, mainLocation_id: int, mainLocation_name: string, absenceStart: \DateTimeImmutable}>
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findAllAsArray(string $lang): iterable;
/**
* Find a list of permissions associated to each users.
*
* The main purpose for this method is to provide a lightweight list of all permissions group and center
* associated to each user.
*
* @return iterable<array{id: int, username: string, email: string, enabled: bool, center_id: int, center_name: string, permissionsGroup_id: int, permissionsGroup_name: string}>
*/
public function findAllUserACLAsArray(): iterable;
/**
* @return array|User[]
@@ -53,8 +61,6 @@ interface UserRepositoryInterface extends ObjectRepository
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
public function findOneByUsernameOrEmail(string $pattern): ?User;
/**
@@ -68,6 +74,4 @@ interface UserRepositoryInterface extends ObjectRepository
* @param mixed $flag
*/
public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
public function getClassName(): string;
}

View File

@@ -39,8 +39,13 @@
{{ 'This will eventually restrict your possibilities in filtering the data.'|trans }}</p>
<h3 class="m-3">{{ 'Center'|trans }}</h3>
{{ form_widget(form.centers.center) }}
<div class="mb-3 mt-3">
<input id="toggle-check-all" class="btn btn-misc" type= "button" onclick='uncheckAll(this)' value="{{ 'uncheck all centers'|trans|e('html_attr') }}"/>
</div>
{% if form.centers.regroupment is defined %}
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
{{ form_widget(form.centers.regroupment) }}
@@ -53,3 +58,15 @@
</div>
{% endblock content %}
{% block js %}
<script>
const uncheckAll = () => {
const allCenters = document.getElementsByName('centers[center][]');
allCenters.forEach(checkbox => checkbox.checked = false)
}
</script>
{% endblock js %}

View File

@@ -98,6 +98,18 @@
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
<li>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-download"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ path('chill_main_users_export_list') }}">{{ 'admin.users.export_list_csv'|trans }}</a></li>
<li><a class="dropdown-item" href="{{ path('chill_main_users_export_permissions') }}">{{ 'admin.users.export_permissions_csv'|trans }}</a></li>
</ul>
</div>
</li>
<li>
<a href="{{ path('chill_crud_admin_user_new') }}" class="btn btn-create">{{ 'Create'|trans }}</a>
</li>

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Service\EntityInfo;
use Doctrine\DBAL\Connection;
class ViewEntityInfoManager
{
public function __construct(
/**
* @var ViewEntityInfoProviderInterface[]
*/
private iterable $vienEntityInfoProviders,
private Connection $connection,
) {
}
public function synchronizeOnDB(): void
{
$this->connection->transactional(function (Connection $conn): void {
foreach ($this->vienEntityInfoProviders as $viewProvider) {
foreach ($this->createOrReplaceViewSQL($viewProvider, $viewProvider->getViewName()) as $sql) {
$conn->executeQuery($sql);
}
}
});
}
/**
* @return array<string>
*/
private function createOrReplaceViewSQL(ViewEntityInfoProviderInterface $viewProvider, string $viewName): array
{
return [
"DROP VIEW IF EXISTS {$viewName}",
sprintf("CREATE VIEW {$viewName} AS %s", $viewProvider->getViewQuery())
];
}
}

View File

@@ -0,0 +1,19 @@
<?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\EntityInfo;
interface ViewEntityInfoProviderInterface
{
public function getViewQuery(): string;
public function getViewName(): string;
}

View File

@@ -1,6 +1,9 @@
parameters:
# cl_chill_main.example.class: Chill\MainBundle\Example
imports:
- ./services/clock.yaml
services:
_defaults:
autowire: true
@@ -118,3 +121,7 @@ services:
lazy: true
arguments:
$jobs: !tagged_iterator chill_main.cron_job
Chill\MainBundle\Service\EntityInfo\ViewEntityInfoManager:
arguments:
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider

View File

@@ -0,0 +1,4 @@
# temporary, waiting for symfony 6.0 to load clock
services:
Symfony\Component\Clock\NativeClock: ~
Symfony\Component\Clock\ClockInterface: '@Symfony\Component\Clock\NativeClock'

View File

@@ -67,3 +67,7 @@ services:
autowire: true
tags:
- {name: console.command }
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
tags:
- {name: console.command}

View File

@@ -37,3 +37,6 @@ services:
Chill\MainBundle\Controller\RegroupmentController:
autowire: true
autoconfigure: true
Chill\MainBundle\Controller\UserExportController:
tags: ['controller.service_arguments']

View File

@@ -285,6 +285,8 @@ The export will contains only data from the picked centers.: L'export ne contien
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis.
Go to export options: Vers la préparation de l'export
Pick aggregated centers: Regroupement de centres
uncheck all centers: Désélectionner tous les centres
check all centers: Sélectionner tous les centres
# export creation step 'export' : choose aggregators, filtering and formatter
Formatter: Mise en forme
Choose the formatter: Choisissez le format d'export voulu.
@@ -564,6 +566,7 @@ export:
_as_string: Adresse formattée
confidential: Adresse confidentielle ?
isNoAddress: Adresse incomplète ?
steps: Escaliers
_lat: Latitude
_lon: Longitude
@@ -609,3 +612,32 @@ absence:
You are listed as absent, as of: Votre absence est indiquée à partir du
No absence listed: Aucune absence indiquée.
Is absent: Absent?
admin:
users:
export_list_csv: Liste des utilisateurs (format CSV)
export_permissions_csv: Association utilisateurs - groupes de permissions - centre (format CSV)
export:
id: Identifiant
username: Nom d'utilisateur
email: Courriel
enabled: Activé
civility_id: Identifiant civilité
civility_abbreviation: Abbréviation civilité
civility_name: Civilité
label: Label
mainCenter_id: Identifiant centre principal
mainCenter_name: Centre principal
mainScope_id: Identifiant service principal
mainScope_name: Service principal
userJob_id: Identifiant métier
userJob_name: Métier
currentLocation_id: Identifiant localisation actuelle
currentLocation_name: Localisation actuelle
mainLocation_id: Identifiant localisation principale
mainLocation_name: Localisation principale
absenceStart: Absent à partir du
center_id: Identifiant du centre
center_name: Centre
permissionsGroup_id: Identifiant du groupe de permissions
permissionsGroup_name: Groupe de permissions

View File

@@ -51,7 +51,10 @@ class UserRefEventSubscriber implements EventSubscriberInterface
public function onStateEntered(EnteredEvent $enteredEvent): void
{
if ($enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)) {
if (
$enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)
and $enteredEvent->getTransition()->getName() === 'confirm'
) {
$this->onPeriodConfirmed($enteredEvent->getSubject());
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\AccompanyingPeriod\Lifecycle;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\Entity\CronJobExecution;
use Symfony\Component\Clock\ClockInterface;
readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
{
public function __construct(
private ClockInterface $clock,
private AccompanyingPeriodStepChangeRequestor $requestor,
) {
}
public function canRun(?CronJobExecution $cronJobExecution): bool
{
$now = $this->clock->now();
if (null !== $cronJobExecution && $now->sub(new \DateInterval('P1D')) < $cronJobExecution->getLastStart()) {
return false;
}
return in_array((int) $now->format('H'), [1, 2, 3, 4, 5, 6], true);
}
public function getKey(): string
{
return 'accompanying-period-step-change';
}
public function run(): void
{
($this->requestor)();
}
}

View File

@@ -0,0 +1,37 @@
<?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\AccompanyingPeriod\Lifecycle;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
#[AsMessageHandler]
class AccompanyingPeriodStepChangeMessageHandler implements MessageHandlerInterface
{
private const LOG_PREFIX = '[accompanying period step change message handler] ';
public function __construct(
private AccompanyingPeriodRepository $accompanyingPeriodRepository,
private AccompanyingPeriodStepChanger $changer,
) {
}
public function __invoke(AccompanyingPeriodStepChangeRequestMessage $message): void
{
if (null === $period = $this->accompanyingPeriodRepository->find($message->getPeriodId())) {
throw new \RuntimeException(self::LOG_PREFIX . 'Could not find period with this id: '. $message->getPeriodId());
}
($this->changer)($period, $message->getTransition());
}
}

View File

@@ -0,0 +1,47 @@
<?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\AccompanyingPeriod\Lifecycle;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
/**
* Message which will request a change in the step of accompanying period
*/
class AccompanyingPeriodStepChangeRequestMessage
{
private int $periodId;
public function __construct(
AccompanyingPeriod|int $period,
private string $transition,
) {
if (is_int($period)) {
$this->periodId = $period;
} else {
if (null !== $id = $period->getId()) {
$this->periodId = $id;
}
throw new \LogicException("This AccompanyingPeriod does not have and id yet");
}
}
public function getPeriodId(): int
{
return $this->periodId;
}
public function getTransition(): string
{
return $this->transition;
}
}

View File

@@ -0,0 +1,87 @@
<?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\AccompanyingPeriod\Lifecycle;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodInfoRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* Gather all the accompanying period which needs a change in step
*/
class AccompanyingPeriodStepChangeRequestor
{
private \DateInterval $intervalForShortInactive;
private \DateInterval $intervalForLongInactive;
private bool $isMarkInactive;
public function __construct(
private AccompanyingPeriodInfoRepositoryInterface $accompanyingPeriodInfoRepository,
private LoggerInterface $logger,
private MessageBusInterface $messageBus,
ParameterBagInterface $parameterBag,
) {
$config = $parameterBag->get('chill_person')['accompanying_period_lifecycle_delays'];
$this->isMarkInactive = $config['mark_inactive'];
$this->intervalForShortInactive = new \DateInterval($config['mark_inactive_short_after']);
$this->intervalForLongInactive = new \DateInterval($config['mark_inactive_long_after']);
}
public function __invoke(): void
{
if (!$this->isMarkInactive) {
return;
}
// get the oldest ones first
foreach (
$olders = $this->accompanyingPeriodInfoRepository->findAccompanyingPeriodIdInactiveAfter(
$this->intervalForLongInactive,
[AccompanyingPeriod::STEP_CONFIRMED, AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT]
) as $accompanyingPeriodId
) {
$this->logger->debug('request mark period as inactive_short', ['period' => $accompanyingPeriodId]);
$this->messageBus->dispatch(new AccompanyingPeriodStepChangeRequestMessage($accompanyingPeriodId, 'mark_inactive_long'));
}
// the newest
foreach (
$this->accompanyingPeriodInfoRepository->findAccompanyingPeriodIdInactiveAfter(
$this->intervalForShortInactive,
[AccompanyingPeriod::STEP_CONFIRMED]
) as $accompanyingPeriodId
) {
if (in_array($accompanyingPeriodId, $olders, true)) {
continue;
}
$this->logger->debug('request mark period as inactive_long', ['period' => $accompanyingPeriodId]);
$this->messageBus->dispatch(new AccompanyingPeriodStepChangeRequestMessage($accompanyingPeriodId, 'mark_inactive_short'));
}
// a new event has been created => remove inactive long, or short
foreach (
$this->accompanyingPeriodInfoRepository->findAccompanyingPeriodIdActiveSince(
$this->intervalForShortInactive,
[AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT, AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG]
) as $accompanyingPeriodId
) {
$this->logger->debug('request mark period as active', ['period' => $accompanyingPeriodId]);
$this->messageBus->dispatch(new AccompanyingPeriodStepChangeRequestMessage($accompanyingPeriodId, 'mark_active'));
}
}
}

View File

@@ -0,0 +1,58 @@
<?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\AccompanyingPeriod\Lifecycle;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Workflow\Registry;
/**
* Change the step of an accompanying period
*
* This should be invoked through scripts (not in the in context of an http request, or an
* action from a user).
*/
class AccompanyingPeriodStepChanger
{
private const LOG_PREFIX = '[AccompanyingPeriodStepChanger] ';
public function __construct(
private EntityManagerInterface $entityManager,
private LoggerInterface $logger,
private Registry $workflowRegistry,
) {
}
public function __invoke(AccompanyingPeriod $period, string $transition, ?string $workflowName = null): void
{
$workflow = $this->workflowRegistry->get($period, $workflowName);
if (!$workflow->can($period, $transition)) {
$this->logger->info(self::LOG_PREFIX . 'not able to apply the transition on period', [
'period_id' => $period->getId(),
'transition' => $transition
]);
return;
}
$workflow->apply($period, $transition);
$this->entityManager->flush();
$this->logger->info(self::LOG_PREFIX . 'could apply a transition', [
'period_id' => $period->getId(),
'transition' => $transition
]);
}
}

View File

@@ -13,7 +13,11 @@ namespace Chill\PersonBundle\Actions\Remove;
use Chill\PersonBundle\Actions\ActionEvent;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Relationships\Relationship;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -42,7 +46,7 @@ class PersonMove
protected $eventDispatcher;
public function __construct(
EntityManagerInterface $em,
EntityManagerInterface $em,
EventDispatcherInterface $eventDispatcher
) {
$this->em = $em;
@@ -84,8 +88,11 @@ class PersonMove
}
foreach ($metadata->getAssociationMappings() as $field => $mapping) {
if (Person::class === $mapping['targetEntity']) {
if (in_array($metadata->getName(), $toDelete, true)) {
if (in_array($mapping['sourceEntity'], $this->getIgnoredEntities(), true)) {
continue;
}
if (Person::class === $mapping['targetEntity'] and true === $mapping['isOwningSide']) {
if (in_array($mapping['sourceEntity'], $toDelete, true)) {
$sql = $this->createDeleteSQL($metadata, $from, $field);
$event = new ActionEvent(
$from->getId(),
@@ -120,7 +127,7 @@ class PersonMove
return $sqls;
}
protected function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string
private function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string
{
$mapping = $metadata->getAssociationMapping($field);
@@ -137,26 +144,41 @@ class PersonMove
);
}
protected function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string
private function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string
{
$mapping = $metadata->getAssociationMapping($field);
// Set part of the query, aka <here> in "UPDATE table SET <here> "
$sets = [];
foreach ($mapping['joinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
$conditions = [];
$tableName = '';
foreach ($mapping['joinColumns'] as $columns) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
if (array_key_exists('joinTable', $mapping)) {
$tableName = (null !== ($mapping['joinTable']['schema'] ?? null) ? $mapping['joinTable']['schema'] . '.' : '')
. $mapping['joinTable']['name'];
foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
}
} elseif (array_key_exists('joinColumns', $mapping)) {
$tableName = $this->getTableName($metadata);
foreach ($mapping['joinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
foreach ($mapping['joinColumns'] as $columns) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
}
}
return sprintf(
'UPDATE %s SET %s WHERE %s',
$this->getTableName($metadata),
$tableName,
implode(' ', $sets),
implode(' AND ', $conditions)
);
@@ -166,10 +188,23 @@ class PersonMove
* return an array of classes where entities should be deleted
* instead of moved.
*/
protected function getDeleteEntities(): array
private function getDeleteEntities(): array
{
return [
AccompanyingPeriod::class,
Person\PersonCenterHistory::class,
HouseholdMember::class,
AccompanyingPeriodParticipation::class,
AccompanyingPeriod\AccompanyingPeriodWork::class,
Relationship::class
];
}
private function getIgnoredEntities(): array
{
return [
Person\PersonCurrentAddress::class,
PersonHouseholdAddress::class,
Person\PersonCenterCurrent::class,
];
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle;
use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
use Chill\PersonBundle\Widget\PersonListWidgetFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -26,5 +27,7 @@ class ChillPersonBundle extends Bundle
->addWidgetFactory(new PersonListWidgetFactory());
$container->addCompilerPass(new AccompanyingPeriodTimelineCompilerPass());
$container->registerForAutoconfiguration(AccompanyingPeriodInfoUnionQueryPartInterface::class)
->addTag('chill_person.accompanying_period_info_part');
}
}

View File

@@ -42,10 +42,18 @@ final class ImportSocialWorkMetadata extends Command
protected function configure()
{
$description = 'Imports a structured table containing social issues, social actions, objectives, results and evaluations.';
$help = 'File to csv format, no headers, semi-colon as delimiter, datas sorted by alphabetical order, column after column.'. PHP_EOL
. 'Columns are: social issues parent, social issues child, social actions parent, social actions child, goals, results, evaluations.'. PHP_EOL
. PHP_EOL
. 'See social_work_metadata.csv as example.'. PHP_EOL;
$this
->setName('chill:person:import-socialwork')
->addOption('filepath', 'f', InputOption::VALUE_REQUIRED, 'The file to import.')
->addOption('language', 'l', InputOption::VALUE_OPTIONAL, 'The default language');
->addOption('language', 'l', InputOption::VALUE_OPTIONAL, 'The default language')
->setDescription($description)
->setHelp($help);
}
protected function execute(InputInterface $input, OutputInterface $output)

View File

@@ -247,7 +247,7 @@ class PersonDuplicateController extends Controller
);
$duplicatePersons = $this->similarPersonMatcher->
matchPerson($person, $personNotDuplicateRepository, 0.5, SimilarPersonMatcher::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL);
matchPerson($person, 0.5, SimilarPersonMatcher::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL, false);
$notDuplicatePersons = $personNotDuplicateRepository->findNotDuplicatePerson($person);
@@ -264,14 +264,14 @@ class PersonDuplicateController extends Controller
$nb_activity = $em->getRepository(Activity::class)->findBy(['person' => $id]);
$nb_document = $em->getRepository(PersonDocument::class)->findBy(['person' => $id]);
$nb_event = $em->getRepository(Participation::class)->findBy(['person' => $id]);
// $nb_event = $em->getRepository(Participation::class)->findBy(['person' => $id]);
$nb_task = $em->getRepository(SingleTask::class)->countByParameters(['person' => $id]);
$person = $em->getRepository(Person::class)->findOneBy(['id' => $id]);
return [
'nb_activity' => count($nb_activity),
'nb_document' => count($nb_document),
'nb_event' => count($nb_event),
// 'nb_event' => count($nb_event),
'nb_task' => $nb_task,
'nb_addresses' => count($person->getAddresses()),
];

View File

@@ -15,6 +15,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Controller\HouseholdCompositionTypeApiController;
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
@@ -1010,18 +1011,42 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
'initial_marking' => 'DRAFT',
'places' => [
'DRAFT',
'CONFIRMED',
'CLOSED',
AccompanyingPeriod::STEP_DRAFT,
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
AccompanyingPeriod::STEP_CLOSED,
],
'transitions' => [
'confirm' => [
'from' => 'DRAFT',
'to' => 'CONFIRMED',
'from' => AccompanyingPeriod::STEP_DRAFT,
'to' => AccompanyingPeriod::STEP_CONFIRMED,
],
'mark_inactive_short' => [
'from' => AccompanyingPeriod::STEP_CONFIRMED,
'to' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
],
'mark_inactive_long' => [
'from' => [
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT
],
'to' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
],
'mark_active' => [
'from' => [
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
],
'to' => AccompanyingPeriod::STEP_CONFIRMED
],
'close' => [
'from' => 'CONFIRMED',
'to' => 'CLOSED',
'from' => [
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
],
'to' => AccompanyingPeriod::STEP_CLOSED,
],
],
],

View File

@@ -128,6 +128,15 @@ class Configuration implements ConfigurationInterface
->info('Can we have more than one simultaneous accompanying period in the same time. Default false.')
->defaultValue(false)
->end()
->arrayNode('accompanying_period_lifecycle_delays')
->addDefaultsIfNotSet()
->info('Delays before marking an accompanying period as inactive')
->children()
->booleanNode('mark_inactive')->defaultTrue()->end()
->scalarNode('mark_inactive_short_after')->defaultValue('P6M')->end()
->scalarNode('mark_inactive_long_after')->defaultValue('P2Y')->end()
->end()
->end() // end of 'accompanying_period_lifecycle_delays
->end() // children of 'root', parent = root
;

View File

@@ -109,6 +109,24 @@ class AccompanyingPeriod implements
*/
public const STEP_CONFIRMED = 'CONFIRMED';
/**
* Mark an accompanying period as confirmed, but inactive
*
* this means that the accompanying period **is**
* confirmed, but no activity (Activity, AccompanyingPeriod, ...)
* has been associated, or updated, within this accompanying period.
*/
public const STEP_CONFIRMED_INACTIVE_SHORT = 'CONFIRMED_INACTIVE_SHORT';
/**
* Mark an accompanying period as confirmed, but inactive
*
* this means that the accompanying period **is**
* confirmed, but no activity (Activity, AccompanyingPeriod, ...)
* has been associated, or updated, within this accompanying period.
*/
public const STEP_CONFIRMED_INACTIVE_LONG = 'CONFIRMED_INACTIVE_LONG';
/**
* Mark an accompanying period as "draft".
*
@@ -340,6 +358,7 @@ class AccompanyingPeriod implements
/**
* @ORM\Column(type="string", length=32, nullable=true)
* @Groups({"read"})
* @var AccompanyingPeriod::STEP_*
*/
private string $step = self::STEP_DRAFT;
@@ -712,11 +731,9 @@ class AccompanyingPeriod implements
if ($this->getStep() === self::STEP_DRAFT) {
return [[self::STEP_DRAFT]];
}
if ($this->getStep() === self::STEP_CONFIRMED) {
if (str_starts_with($this->getStep(), 'CONFIRM')) {
return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
}
if ($this->getStep() === self::STEP_CLOSED) {
return [[self::STEP_DRAFT, self::STEP_CONFIRMED, self::STEP_CLOSED]];
}

View File

@@ -0,0 +1,79 @@
<?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\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\Mapping as ORM;
/**
* Informations about AccompanyingPeriod
*
* This entity allow access to some basic information about the AccompanyingPeriod. It is
* populated from a SQL view, dynamically build from various sources.
*
* Usage:
*
* - get the user involved with an accompanying period
*
* @ORM\Entity()
* @ORM\Table(name="view_chill_person_accompanying_period_info")
*/
class AccompanyingPeriodInfo
{
public function __construct(
/**
* @var AccompanyingPeriod
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
*/
public readonly AccompanyingPeriod $accompanyingPeriod,
/**
* @var string
* @ORM\Column(type="text")
* @ORM\Id
*/
public readonly string $relatedEntity,
/**
* @var int
* @ORM\Column(type="integer")
* @ORM\Id
*/
public readonly int $relatedEntityId,
/**
* @var User
* @ORM\ManyToOne(targetEntity=User::class)
*/
public readonly ?User $user,
/**
* @var \DateTimeImmutable
* @ORM\Column(type="datetime_immutable")
*/
public readonly \DateTimeImmutable $infoDate,
/**
* @var array
* @ORM\Column(type="json")
*/
public readonly array $metadata,
/**
* @var string
* @ORM\Column(type="text")
*/
public readonly string $discriminator,
) {
}
}

View File

@@ -213,6 +213,10 @@ class Household
return null;
}
/**
* @Serializer\Groups({"docgen:read"})
* @Serializer\SerializedName("current_composition")
*/
public function getCurrentComposition(?DateTimeImmutable $at = null): ?HouseholdComposition
{
$at ??= new DateTimeImmutable('today');

View File

@@ -44,6 +44,7 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Assert\GreaterThanOrEqual(propertyPath="startDate", groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/
private ?DateTimeImmutable $endDate = null;
@@ -56,6 +57,7 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
/**
* @ORM\ManyToOne(targetEntity=HouseholdCompositionType::class)
* @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"docgen:read"})
*/
private ?HouseholdCompositionType $householdCompositionType = null;
@@ -71,12 +73,14 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
* @ORM\Column(type="integer", nullable=true, options={"default": null})
* @Assert\NotNull
* @Assert\GreaterThanOrEqual(0, groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/
private ?int $numberOfChildren = null;
/**
* @ORM\Column(type="date_immutable", nullable=false)
* @Assert\NotNull(groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/
private ?DateTimeImmutable $startDate = null;

View File

@@ -98,7 +98,7 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface
'acp_geog_units'
);
$qb->andWhere($qb->expr()->eq('acp_geog_units.layer', ':acp_geog_unit_layer'));
$qb->andWhere($qb->expr()->in('acp_geog_units.layer', ':acp_geog_unit_layer'));
$qb->setParameter('acp_geog_unit_layer', $data['level']);
@@ -129,6 +129,8 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface
'class' => GeographicalUnitLayer::class,
'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(),
'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()),
'multiple' => true,
'expanded' => true,
]);
}

View File

@@ -86,13 +86,19 @@ final class StepAggregator implements AggregatorInterface
return function ($value): string {
switch ($value) {
case AccompanyingPeriod::STEP_DRAFT:
return $this->translator->trans('Draft');
return $this->translator->trans('course.draft');
case AccompanyingPeriod::STEP_CONFIRMED:
return $this->translator->trans('Confirmed');
return $this->translator->trans('course.confirmed');
case AccompanyingPeriod::STEP_CLOSED:
return $this->translator->trans('Closed');
return $this->translator->trans('course.closed');
case AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT:
return $this->translator->trans('course.inactive_short');
case AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG:
return $this->translator->trans('course.inactive_long');
case '_header':
return 'Step';

View File

@@ -101,6 +101,8 @@ class GeographicalUnitAggregator implements AggregatorInterface
'class' => GeographicalUnitLayer::class,
'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(),
'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()),
'multiple' => true,
'expanded' => true,
]);
}

View File

@@ -41,6 +41,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function strlen;
class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
@@ -100,6 +101,8 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
private UserHelper $userHelper;
public function __construct(
@@ -113,6 +116,7 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
SocialIssueRepository $socialIssueRepository,
SocialIssueRender $socialIssueRender,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatorInterface $translator,
RollingDateConverterInterface $rollingDateConverter,
UserHelper $userHelper
) {
@@ -126,6 +130,7 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
$this->rollingDateConverter = $rollingDateConverter;
$this->userHelper = $userHelper;
}
@@ -250,6 +255,27 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
);
};
case 'step':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.step',
null => '',
AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'),
AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'),
AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'),
default => $value,
};
case 'intensity':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.intensity',
null => '',
AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'),
AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'),
default => $value,
};
default:
return static function ($value) use ($key) {
if ('_header' === $value) {

View File

@@ -107,7 +107,6 @@ class ListHouseholdInPeriod implements ListInterface, GroupedExportInterface
return $this->aggregateStringHelper->getLabelMulti($key, $values, 'export.list.household.' . $key);
case 'compositionType':
//dump($values);
return $this->translatableStringHelper->getLabel($key, $values, 'export.list.household.' . $key);
default:

View File

@@ -0,0 +1,91 @@
<?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\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\DateIntervalType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodInfo;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Filter accompanying course which have a row in AccompanyingPeriodInfo within the given
* interval
*/
final readonly class HavingAnAccompanyingPeriodInfoWithinDatesFilter implements FilterInterface
{
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.course.having_info_within_interval.start_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.course.having_info_within_interval.end_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
;
}
public function getTitle(): string
{
return 'export.filter.course.having_info_within_interval.title';
}
public function describeAction($data, $format = 'string'): array
{
return [
'export.filter.course.having_info_within_interval.Only course with events between %startDate% and %endDate%',
[
'%startDate%' => $this->rollingDateConverter->convert($data['start_date'])->format('d-m-Y'),
'%endDate%' => $this->rollingDateConverter->convert($data['end_date'])->format('d-m-Y'),
]
];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$ai = 'having_ai_within_interval_acc_info';
$as = 'having_ai_within_interval_start_date';
$ae = 'having_ai_within_interval_end_date';
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . AccompanyingPeriodInfo::class . " {$ai} WHERE {$ai}.infoDate BETWEEN :{$as} AND :{$ae} AND IDENTITY({$ai}.accompanyingPeriod) = acp.id"
)
)
->setParameter($as, $this->rollingDateConverter->convert($data['start_date']), Types::DATETIME_IMMUTABLE)
->setParameter($ae, $this->rollingDateConverter->convert($data['end_date']), Types::DATETIME_IMMUTABLE);
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
}

View File

@@ -32,9 +32,11 @@ class StepFilter implements FilterInterface
private const P = 'acp_step_filter_date';
private const STEPS = [
'Draft' => AccompanyingPeriod::STEP_DRAFT,
'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
'Closed' => AccompanyingPeriod::STEP_CLOSED,
'course.draft' => AccompanyingPeriod::STEP_DRAFT,
'course.confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
'course.closed' => AccompanyingPeriod::STEP_CLOSED,
'course.inactive_short' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
'course.inactive_long' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
];
private RollingDateConverterInterface $rollingDateConverter;
@@ -96,7 +98,7 @@ class StepFilter implements FilterInterface
'data' => self::DEFAULT_CHOICE,
])
->add('calc_date', PickRollingDateType::class, [
'label' => 'export.acp.filter.by_step.date_calc',
'label' => 'export.filter.course.by_step.date_calc',
'data' => new RollingDate(RollingDate::T_TODAY),
]);
}

View File

@@ -0,0 +1,88 @@
<?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\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Filter course where a user is "working" on it
*
* Makes use of AccompanyingPeriodInfo
*/
readonly class UserWorkingOnCourseFilter implements FilterInterface
{
private const AI_ALIAS = 'user_working_on_course_filter_acc_info';
private const AI_USERS = 'user_working_on_course_filter_users';
public function __construct(
private UserRender $userRender,
) {
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('users', PickUserDynamicType::class, [
'multiple' => true,
]);
}
public function getTitle(): string
{
return 'export.filter.course.by_user_working.title';
}
public function describeAction($data, $format = 'string'): array
{
return [
'export.filter.course.by_user_working.Filtered by user working on course: only %users%', [
'%users%' => implode(
', ',
array_map(
fn (User $u) => $this->userRender->renderString($u, []),
$data['users']
)
),
],
];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->andWhere(
$qb->expr()->exists(
"SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " " . self::AI_ALIAS . " " .
"WHERE " . self::AI_ALIAS . ".user IN (:" . self::AI_USERS .") AND IDENTITY(" . self::AI_ALIAS . ".accompanyingPeriod) = acp.id"
)
)
->setParameter(self::AI_USERS, $data['users'])
;
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
}

View File

@@ -0,0 +1,114 @@
<?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\Export\Filter\SocialWorkFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements FilterInterface
{
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.work.end_between_dates.start_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.work.end_between_dates.end_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
->add('keep_null', CheckboxType::class, [
'label' => 'export.filter.work.end_between_dates.keep_null',
'help' => 'export.filter.work.end_between_dates.keep_null_help',
])
;
}
public function getTitle(): string
{
return 'export.filter.work.end_between_dates.title';
}
public function describeAction($data, $format = 'string'): array
{
return [
'export.filter.work.end_between_dates.Only where end date is between %endDate% and %endDate%',
[
'%startDate%' => null !== $data['start_date'] ? $this->rollingDateConverter->convert($data['start_date'])->format('d-m-Y') : '',
'%endDate%' => null !== $data['end_date'] ? $this->rollingDateConverter->convert($data['end_date'])->format('d-m-Y') : '',
]
];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$as = 'acc_pe_work_end_between_filter_start';
$ae = 'acc_pe_work_end_between_filter_end';
$start = match ($data['keep_null']) {
true => $qb->expr()->orX(
$qb->expr()->lte('acpw.endDate', ':'.$ae),
$qb->expr()->isNull('acpw.endDate')
),
false => $qb->expr()->andX(
$qb->expr()->lte('acpw.endDate', ':'.$ae),
$qb->expr()->isNotNull('acpw.endDate')
),
default => throw new \LogicException("This value is not supported"),
};
$end = match ($data['keep_null']) {
true => $qb->expr()->orX(
$qb->expr()->gt('acpw.endDate', ':'.$as),
$qb->expr()->isNull('acpw.endDate')
),
false => $qb->expr()->andX(
$qb->expr()->gt('acpw.endDate', ':'.$as),
$qb->expr()->isNotNull('acpw.endDate')
),
default => throw new \LogicException("This value is not supported"),
};
if (null !== $data['start_date']) {
$qb
->andWhere($start)
->setParameter($as, $this->rollingDateConverter->convert($data['start_date']));
}
if (null !== $data['end_date']) {
$qb
->andWhere($end)
->setParameter($ae, $this->rollingDateConverter->convert($data['end_date']));
}
}
public function applyOn(): string
{
return Declarations::SOCIAL_WORK_ACTION_TYPE;
}
}

View File

@@ -0,0 +1,114 @@
<?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\Export\Filter\SocialWorkFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class AccompanyingPeriodWorkStartDateBetweenDateFilter implements FilterInterface
{
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.work.start_between_dates.start_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.work.start_between_dates.end_date',
'data' => new RollingDate(RollingDate::T_TODAY),
])
->add('keep_null', CheckboxType::class, [
'label' => 'export.filter.work.start_between_dates.keep_null',
'help' => 'export.filter.work.start_between_dates.keep_null_help',
])
;
}
public function getTitle(): string
{
return 'export.filter.work.start_between_dates.title';
}
public function describeAction($data, $format = 'string'): array
{
return [
'export.filter.work.start_between_dates.Only where start date is between %startDate% and %endDate%',
[
'%startDate%' => null !== $data['start_date'] ? $this->rollingDateConverter->convert($data['start_date'])->format('d-m-Y') : '',
'%endDate%' => null !== $data['end_date'] ? $this->rollingDateConverter->convert($data['end_date'])->format('d-m-Y') : '',
]
];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$as = 'acc_pe_work_start_between_filter_start';
$ae = 'acc_pe_work_start_between_filter_end';
$start = match ($data['keep_null']) {
true => $qb->expr()->orX(
$qb->expr()->lte('acpw.startDate', ':'.$ae),
$qb->expr()->isNull('acpw.startDate')
),
false => $qb->expr()->andX(
$qb->expr()->lte('acpw.startDate', ':'.$ae),
$qb->expr()->isNotNull('acpw.startDate')
),
default => throw new \LogicException("This value is not supported"),
};
$end = match ($data['keep_null']) {
true => $qb->expr()->orX(
$qb->expr()->gt('acpw.startDate', ':'.$as),
$qb->expr()->isNull('acpw.startDate')
),
false => $qb->expr()->andX(
$qb->expr()->gt('acpw.startDate', ':'.$as),
$qb->expr()->isNotNull('acpw.startDate')
),
default => throw new \LogicException("This value is not supported"),
};
if (null !== $data['start_date']) {
$qb
->andWhere($start)
->setParameter($as, $this->rollingDateConverter->convert($data['start_date']));
}
if (null !== $data['end_date']) {
$qb
->andWhere($end)
->setParameter($ae, $this->rollingDateConverter->convert($data['end_date']));
}
}
public function applyOn(): string
{
return Declarations::SOCIAL_WORK_ACTION_TYPE;
}
}

View File

@@ -15,6 +15,7 @@ use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
@@ -106,17 +107,19 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtras([
'order' => 99999,
]);
/*
$menu->addChild($this->translator->trans('Person duplicate'), [
'route' => 'chill_person_duplicate_view',
'routeParameters' => [
'person_id' => $parameters['person']->getId(),
],
])
->setExtras([
'order' => 99999,
]);
*/
if ($this->security->isGranted(PersonVoter::DUPLICATE, $parameters['person'])) {
$menu->addChild($this->translator->trans('Person duplicate'), [
'route' => 'chill_person_duplicate_view',
'routeParameters' => [
'person_id' => $parameters['person']->getId(),
],
])
->setExtras([
'order' => 99999,
]);
}
if (
'visible' === $this->showAccompanyingPeriod
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])

View File

@@ -0,0 +1,92 @@
<?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\Repository\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodInfo;
use DateInterval;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use LogicException;
use Symfony\Component\Clock\ClockInterface;
readonly class AccompanyingPeriodInfoRepository implements AccompanyingPeriodInfoRepositoryInterface
{
private EntityRepository $entityRepository;
public function __construct(
private ClockInterface $clock,
private EntityManagerInterface $em,
) {
$this->entityRepository = $em->getRepository($this->getClassName());
}
public function findAccompanyingPeriodIdInactiveAfter(DateInterval $interval, array $statuses = []): array
{
$query = $this->em->createQuery();
$baseDql = 'SELECT DISTINCT IDENTITY(ai.accompanyingPeriod) FROM '.AccompanyingPeriodInfo::class.' ai JOIN ai.accompanyingPeriod a WHERE NOT EXISTS
(SELECT 1 FROM ' . AccompanyingPeriodInfo::class . ' aiz WHERE aiz.infoDate > :after AND IDENTITY(aiz.accompanyingPeriod) = IDENTITY(ai.accompanyingPeriod))';
if ([] !== $statuses) {
$dql = $baseDql . ' AND a.step IN (:statuses)';
$query->setParameter('statuses', $statuses);
} else {
$dql = $baseDql;
}
return $query->setDQL($dql)
->setParameter('after', $this->clock->now()->sub($interval))
->getSingleColumnResult();
}
public function findAccompanyingPeriodIdActiveSince(DateInterval $interval, array $statuses = []): array
{
$query = $this->em->createQuery();
$baseDql = 'SELECT DISTINCT IDENTITY(ai.accompanyingPeriod) FROM ' . AccompanyingPeriodInfo::class . ' ai
JOIN ai.accompanyingPeriod a WHERE ai.infoDate > :after';
if ([] !== $statuses) {
$dql = $baseDql . ' AND a.step IN (:statuses)';
$query->setParameter('statuses', $statuses);
} else {
$dql = $baseDql;
}
return $query->setDQL($dql)
->setParameter('after', $this->clock->now()->sub($interval))
->getSingleColumnResult();
}
public function find($id): ?AccompanyingPeriodInfo
{
throw new LogicException("Calling an accompanying period info by his id does not make sense");
}
public function findAll(): array
{
return $this->entityRepository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?AccompanyingPeriodInfo
{
return $this->entityRepository->findOneBy($criteria);
}
public function getClassName(): string
{
return AccompanyingPeriodInfo::class;
}
}

View File

@@ -0,0 +1,38 @@
<?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\Repository\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodInfo;
use Doctrine\Persistence\ObjectRepository;
/**
* @template-extends ObjectRepository<AccompanyingPeriodInfo>
*/
interface AccompanyingPeriodInfoRepositoryInterface extends ObjectRepository
{
/**
* Return a list of id for inactive accompanying periods
*
* @param \DateInterval $interval
* @param list<AccompanyingPeriod::STEP_*> $statuses
* @return list<int>
*/
public function findAccompanyingPeriodIdInactiveAfter(\DateInterval $interval, array $statuses = []): array;
/**
* @param \DateInterval $interval
* @param list<AccompanyingPeriod::STEP_*> $statuses
* @return list<int>
*/
public function findAccompanyingPeriodIdActiveSince(\DateInterval $interval, array $statuses = []): array;
}

View File

@@ -14,7 +14,7 @@
<scopes></scopes>
<referrer></referrer>
<resources></resources>
<start-date v-if="accompanyingCourse.step === 'CONFIRMED'"></start-date>
<start-date v-if="accompanyingCourse.step.startsWith('CONFIRMED')"></start-date>
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
<confirm v-if="accompanyingCourse.step === 'DRAFT'"></confirm>

View File

@@ -11,12 +11,22 @@
{{ $t('course.step.draft') }}
</span>
</span>
<span v-else-if="accompanyingCourse.step === 'CONFIRMED'" class="text-md-end">
<span class="d-md-block mb-md-3">
<span v-else-if="accompanyingCourse.step === 'CONFIRMED' || accompanyingCourse.step === 'CONFIRMED_INACTIVE_SHORT' || accompanyingCourse.step === 'CONFIRMED_INACTIVE_LONG'" class="text-md-end">
<span v-if="accompanyingCourse.step === 'CONFIRMED'" class="d-md-block mb-md-3">
<span class="badge bg-primary">
{{ $t('course.step.active') }}
</span>
</span>
<span v-else-if="accompanyingCourse.step === 'CONFIRMED_INACTIVE_SHORT'" class="d-md-block mb-md-3">
<span class="badge bg-chill-yellow text-primary">
{{ $t('course.step.inactive_short') }}
</span>
</span>
<span v-else-if="accompanyingCourse.step === 'CONFIRMED_INACTIVE_LONG'" class="d-md-block mb-md-3">
<span class="badge bg-chill-pink">
{{ $t('course.step.inactive_long') }}
</span>
</span>
<span class="d-md-block">
<span class="d-md-block ms-3 ms-md-0">
<i>{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}</i>

View File

@@ -21,7 +21,9 @@ const appMessages = {
step: {
draft: "Brouillon",
active: "En file active",
closed: "Cloturé"
closed: "Cloturé",
inactive_short: "Hors file active",
inactive_long: "Pré-archivé",
},
open_at: "ouvert le ",
by: "par ",

View File

@@ -16,11 +16,13 @@
</div>
<div class="wh-col">
{% if period.step == 'DRAFT' %}
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span>
{% elseif period.step == 'CONFIRMED' %}
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
{% else %}
<span class="badge bg-danger">{{- 'Closed'|trans|upper -}}</span>
<span class="badge bg-secondary" style="font-size: 85%;" title="{{ 'course.draft'|trans|e('html_attr') }}">{{ 'course.draft'|trans }}</span>
{% elseif period.step == 'CLOSED' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.closed'|trans|e('html_attr') }}">{{ 'course.closed'|trans }}</span>
{% elseif period.step == 'CONFIRMED_INACTIVE_SHORT' %}
<span class="badge bg-chill-yellow text-primary" style="font-size: 85%;" title="{{ 'course.inactive_short'|trans|e('html_attr') }}">{{ 'course.inactive_short'|trans }}</span>
{% elseif period.step == 'CONFIRMED_INACTIVE_LONG' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.inactive_long'|trans|e('html_attr') }}">{{ 'course.inactive_long'|trans }}</span>
{% endif %}
</div>
</div>

View File

@@ -52,11 +52,13 @@
{% endif %}
{% if acp.step == 'DRAFT' %}
<span class="badge bg-secondary" style="font-size: 85%;" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span>
{% endif %}
{% if acp.step == 'CLOSED' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.closed'|trans }}">{{ 'course.closed'|trans }}</span>
<span class="badge bg-secondary" style="font-size: 85%;" title="{{ 'course.draft'|trans|e('html_attr') }}">{{ 'course.draft'|trans }}</span>
{% elseif acp.step == 'CLOSED' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.closed'|trans|e('html_attr') }}">{{ 'course.closed'|trans }}</span>
{% elseif acp.step == 'CONFIRMED_INACTIVE_SHORT' %}
<span class="badge bg-chill-yellow text-primary" style="font-size: 85%;" title="{{ 'course.inactive_short'|trans|e('html_attr') }}">{{ 'course.inactive_short'|trans }}</span>
{% elseif acp.step == 'CONFIRMED_INACTIVE_LONG' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.inactive_long'|trans|e('html_attr') }}">{{ 'course.inactive_long'|trans }}</span>
{% endif %}
</div>
</div>

View File

@@ -39,7 +39,7 @@
<li><b>{{ person.counters.nb_activity }}</b> {{ (person.counters.nb_activity > 1)? 'échanges' : 'échange' }}</li>
<li><b>{{ person.counters.nb_task }}</b> {{ (person.counters.nb_task > 1)? 'tâches' : 'tâche' }}</li>
<li><b>{{ person.counters.nb_document }}</b> {{ (person.counters.nb_document > 1)? 'documents' : 'document' }}</li>
<li><b>{{ person.counters.nb_event }}</b> {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}</li>
{# <li><b>{{ person.counters.nb_event }}</b> {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}</li>#}
<li><b>{{ person.counters.nb_addresses }}</b> {{ (person.counters.nb_addresses > 1)? 'adresses' : 'adresse' }}</li>
</ul>

View File

@@ -25,7 +25,7 @@
<h1>{{ 'Merge duplicate persons folders'|trans }}</h1>
<div class="col-md-6">
<div class="col-md-11">
<p><b>{{ 'Old person'|trans }}</b>:
{{ 'Old person explain'|trans }}
</p>
@@ -43,7 +43,7 @@
</div>
</div>
<div class="col-md-6">
<div class="col-md-11">
<p><b>{{ 'New person'|trans }}</b>:
{{ 'New person explain'|trans }}
</p>
@@ -63,10 +63,10 @@
{{ form_start(form) }}
<div class="col-md-4 centered">
<div class="col-md-12 centered">
<div class="container-fluid" style="padding-top: 1em;">
<div class="col-1 clear" style="padding-top: 10px;">
<div class="clear" style="padding-top: 10px;">
{{ form_widget(form.confirm) }}
</div>
<div class="col-11">

View File

@@ -3,6 +3,14 @@
{{ 'workflow.SocialAction deleted'|trans }}
</div>
{% else %}
<div class="flex-table accompanying-course-work">
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
'w': work,
'displayAction': false,
'displayContent': 'short',
'itemBlocClass': 'bg-chill-light-gray'
} %}
</div>
{% if display_action is defined and display_action == true %}
<ul class="record_actions">
<li>

View File

@@ -18,6 +18,7 @@ use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@@ -119,6 +120,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
->generate(self::class)
->addCheckFor(null, [self::CREATE, self::REASSIGN_BULK])
->addCheckFor(AccompanyingPeriod::class, [self::TOGGLE_CONFIDENTIAL, ...self::ALL])
->addCheckFor(Household::class, [self::SEE])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo;
/**
* Build a list of different Query parts into a single query
*/
class AccompanyingPeriodInfoQueryBuilder
{
private const BASE_QUERY = <<<'SQL'
SELECT
{period_id_column} AS accompanyingperiod_id,
'{related_entity_column_id}' AS relatedentity,
{related_entity_id_column_id} AS relatedentityid,
{user_id} AS user_id,
{datetime} AS infodate,
'{discriminator}' AS discriminator,
{metadata} AS metadata
FROM {from_statement}
{where_statement}
SQL;
public function buildQuery(AccompanyingPeriodInfoUnionQueryPartInterface $query): string
{
return strtr(
self::BASE_QUERY,
[
'{period_id_column}' => $query->getAccompanyingPeriodIdColumn(),
'{related_entity_column_id}' => $query->getRelatedEntityColumn(),
'{related_entity_id_column_id}' => $query->getRelatedEntityIdColumn(),
'{user_id}' => $query->getUserIdColumn(),
'{datetime}' => $query->getDateTimeColumn(),
'{discriminator}' => $query->getDiscriminator(),
'{metadata}' => $query->getMetadataColumn(),
'{from_statement}' => $query->getFromStatement(),
'{where_statement}' => '' === $query->getWhereClause() ? '' : 'WHERE '.$query->getWhereClause(),
]
);
}
}

View File

@@ -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 Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodStartQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'a.id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriod::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'a.id';
}
public function getUserIdColumn(): string
{
return 'NULL';
}
public function getDateTimeColumn(): string
{
return 'a.openingDate';
}
public function getDiscriminator(): string
{
return 'accompanying_period_start';
}
public function getMetadataColumn(): string
{
return '\'{}\'::jsonb';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period a';
}
public function getWhereClause(): string
{
return '';
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEndQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'w.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWork::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'w.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'w.endDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_end';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return 'w.endDate IS NOT NULL';
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationDocumentUpdateQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'cpapw.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'e.id';
}
public function getUserIdColumn(): string
{
return 'e.updatedby_id';
}
public function getDateTimeColumn(): string
{
return 'e.updatedAt';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_updated_at';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return 'e.updatedAt IS NOT NULL';
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationMaxQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'cpapw.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'e.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'e.maxDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_start';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return 'e.maxDate IS NOT NULL';
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationStartQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'cpapw.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'e.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'e.startDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_start';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return '';
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationUpdateQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'cpapw.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWorkEvaluationDocument::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'doc.id';
}
public function getUserIdColumn(): string
{
return 'doc.updatedby_id';
}
public function getDateTimeColumn(): string
{
return 'doc.updatedAt';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_document_updated_at';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation_document doc
JOIN chill_person_accompanying_period_work_evaluation e ON doc.accompanyingperiodworkevaluation_id = e.id
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return 'doc.updatedAt IS NOT NULL';
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationWarningDateQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'cpapw.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'e.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'e.maxDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_max';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return 'e.maxDate IS NOT NULL';
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkStartQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'w.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWork::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'w.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'w.startDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_start';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return '';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo;
interface AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string;
/**
* @return class-string
*/
public function getRelatedEntityColumn(): string;
public function getRelatedEntityIdColumn(): string;
public function getUserIdColumn(): string;
public function getDateTimeColumn(): string;
public function getDiscriminator(): string;
public function getMetadataColumn(): string;
public function getFromStatement(): string;
public function getWhereClause(): string;
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo;
use Chill\MainBundle\Service\EntityInfo\ViewEntityInfoProviderInterface;
class AccompanyingPeriodViewEntityInfoProvider implements ViewEntityInfoProviderInterface
{
public function __construct(
/**
* @var AccompanyingPeriodInfoUnionQueryPartInterface[]
*/
private iterable $unions,
private AccompanyingPeriodInfoQueryBuilder $builder,
) {
}
public function getViewQuery(): string
{
return implode(
' UNION ',
array_map(
fn (AccompanyingPeriodInfoUnionQueryPartInterface $part) => $this->builder->buildQuery($part),
iterator_to_array($this->unions)
)
);
}
public function getViewName(): string
{
return 'view_chill_person_accompanying_period_info';
}
}

View File

@@ -0,0 +1,54 @@
<?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 AccompanyingPeriod\Lifecycle;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\PersonBundle\AccompanyingPeriod\Lifecycle\AccompanyingPeriodStepChangeCronjob;
use Chill\PersonBundle\AccompanyingPeriod\Lifecycle\AccompanyingPeriodStepChangeRequestor;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
/**
* @internal
* @coversNothing
*/
class AccompanyingPeriodStepChangeCronjobTest extends TestCase
{
use ProphecyTrait;
/**
* @dataProvider provideRunTimes
*/
public function testCanRun(string $datetime, \DateTimeImmutable $lastExecutionStart, bool $canRun): void
{
$requestor = $this->prophesize(AccompanyingPeriodStepChangeRequestor::class);
$clock = new MockClock($datetime);
$cronJob = new AccompanyingPeriodStepChangeCronjob($clock, $requestor->reveal());
$cronJobExecution = (new CronJobExecution($cronJob->getKey()))->setLastStart($lastExecutionStart);
$this->assertEquals($canRun, $cronJob->canRun($cronJobExecution));
}
public function provideRunTimes(): iterable
{
// can run, during the night
yield ['2023-01-15T01:00:00+02:00', new \DateTimeImmutable('2023-01-14T00:00:00+02:00'), true];
// can not run, not during the night
yield ['2023-01-15T10:00:00+02:00', new \DateTimeImmutable('2023-01-14T00:00:00+02:00'), false];
// can not run: not enough elapsed time
yield ['2023-01-15T01:00:00+02:00', new \DateTimeImmutable('2023-01-15T00:30:00+02:00'), false];
}
}

View File

@@ -40,6 +40,8 @@ final class StepFilterTest extends AbstractFilterTest
return [
['accepted_steps' => AccompanyingPeriod::STEP_DRAFT],
['accepted_steps' => AccompanyingPeriod::STEP_CONFIRMED],
['accepted_steps' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG],
['accepted_steps' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT],
['accepted_steps' => AccompanyingPeriod::STEP_CLOSED],
];
}

View File

@@ -78,17 +78,13 @@ final class SocialWorkTypeFilterTest extends AbstractFilterTest
$goals = array_unique($goals);
$results = array_unique($results);
$data = [
return [
[
'actionType' => implode(',', $actions),
'goal' => implode(',', $goals),
'result' => implode(',', $results),
],
];
/// TODO ne fonctionne pas
var_dump($data);
return $data;
}
public function getQueryBuilders(): array

View File

@@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
@@ -39,7 +40,7 @@ final class HouseholdNormalizerTest extends KernelTestCase
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function testNormalizationRecursive()
public function testNormalizationRecursive(): void
{
$person = new Person();
$person->setFirstName('ok')->setLastName('ok');
@@ -67,4 +68,53 @@ final class HouseholdNormalizerTest extends KernelTestCase
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('household', $normalized['type']);
}
/**
* When a household have old members (members which are not "current"),
* the indexes of the household must be reset to numerical and contiguous
* indexes. This ensure that it will be mapped as a list, not as an associative
* array.
*/
public function testHouseholdDocGenNormalizationWithOldMembers(): void
{
$previousPerson = new Person();
$previousPerson->setFirstName('ok')->setLastName('ok');
$this->entityManager->persist($previousPerson);
$member = new HouseholdMember();
$household = new Household();
$position = (new Position())
->setShareHousehold(true)
->setAllowHolder(true);
$member->setPerson($previousPerson)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('1 month ago'))
->setPosition($position);
$household->addMember($member);
$currentPerson1 = new Person();
$currentPerson1->setFirstName('p1')->setLastName('p1');
$this->entityManager->persist($currentPerson1);
$member = new HouseholdMember();
$member->setPerson($currentPerson1)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setPosition($position);
$household->addMember($member);
$normalized = $this->normalizer->normalize(
$household,
'docgen',
[
AbstractNormalizer::GROUPS => ['docgen:read'],
'docgen:expects' => Household::class,
'docgen:person:with-household' => false,
'docgen:person:with-relations' => false,
'docgen:person:with-budget' => false,
]
);
self::assertIsArray($normalized);
self::assertArrayHasKey(0, $normalized['currentMembers']);
}
}

View File

@@ -98,3 +98,7 @@ services:
autowire: true
autoconfigure: true
resource: '../Workflow/'
Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodViewEntityInfoProvider:
arguments:
$unions: !tagged_iterator chill_person.accompanying_period_info_part

View File

@@ -25,6 +25,11 @@ services:
autowire: true
autoconfigure: true
Chill\PersonBundle\AccompanyingPeriod\Lifecycle\:
resource: './../../AccompanyingPeriod/Lifecycle'
autowire: true
autoconfigure: true
Chill\PersonBundle\AccompanyingPeriod\Events\UserRefEventSubscriber:
autowire: true
autoconfigure: true

View File

@@ -128,6 +128,13 @@ services:
tags:
- { name: chill.export_filter, alias: accompanyingcourse_creator_job_filter }
Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserWorkingOnCourseFilter:
tags:
- { name: chill.export_filter, alias: accompanyingcourse_user_working_on_filter }
Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HavingAnAccompanyingPeriodInfoWithinDatesFilter:
tags:
- { name: chill.export_filter, alias: accompanyingcourse_info_within_filter }
## Aggregators
chill.person.export.aggregator_referrer_scope:

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