Compare commits

..

9 Commits

Author SHA1 Message Date
7aa63a25a7 Add signature messenger request serialization and processing
This update introduces a new serializer class for request messages (from messenger component). New features-includes encoding and decoding of request messages and handling unexpected value exceptions. A new test class for the serializer and it also adds functionality to process signature requests in the controller.
2024-06-26 10:05:38 +02:00
610239930b Add serialization groups to PDFPage and PDFSignatureZone properties
The Symfony Serializer groups annotation has been added to all properties of the PDFPage and PDFSignatureZone classes. This change allows the serialization and deserialization process to be group-specific, ensuring only relevant data is processed during these operations.
2024-06-25 13:43:48 +02:00
b65e2c62c4 Merge branch 'signature-app/parse-pdf' into 'signature-app-master'
Add PDF signature zone parsing functionality

See merge request Chill-Projet/chill-bundles!703
2024-06-25 11:27:34 +00:00
89f5231649 Refactor PDFSignatureZoneParser to use float values
This update changes how we handle values in PDFSignatureZoneParser class. Specifically, we've modified the 'MediaBox' and 'PDFSignatureZone' variables to use float values. The helps ensure greater precision, minimize errors, and maintain data consistency across the application.
2024-06-25 13:25:49 +02:00
99818c211d Fix cs: upgrade of php-cs-fixer 2024-06-19 12:18:20 +02:00
a9f0059743 Add PDF signature zone parsing functionality
This update introduces new services into the ChillDocStoreBundle for signature zone parsing within PDFs. The PDFSignatureZoneParser service identifies signature zones within PDF content while the additional classes, PDFPage and PDFSignatureZone, help define these zones and pages. Corresponding tests have also been
2024-06-19 12:17:25 +02:00
5bc542a567 remove symfony/phpunit-bridge 2024-06-19 12:16:51 +02:00
c8ccce83fd add a dependency on smalot/pdfparser to parse signature zone within pdf 2024-06-18 17:47:16 +02:00
4a229ebf6b Initial commit 2024-06-14 15:32:51 +02:00
363 changed files with 1438 additions and 15843 deletions

View File

@@ -0,0 +1,5 @@
kind: Feature
body: '[DX] move async-upload-bundle features into chill-bundles'
time: 2023-12-12T15:48:41.954970271+01:00
custom:
Issue: "221"

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
time: 2024-05-30T16:00:03.440767606+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade CKEditor and refactor configuration with use of typescript
time: 2024-05-31T19:02:42.776662753+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,8 @@
kind: Feature
body: |-
Electronic signature
Implementation of the electronic signature for documents within chill.
time: 2024-06-14T15:32:36.875891692+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Fix resolving of centers for an household, which will fix in turn the access
control
time: 2024-04-10T10:37:36.462484988+02:00
custom:
Issue: ""

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.

View File

@@ -1,5 +0,0 @@
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses

View File

@@ -1,3 +0,0 @@
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats

View File

@@ -1,11 +0,0 @@
## v2.23.0 - 2024-07-23
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export

View File

@@ -6,93 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.23.0 - 2024-07-23
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
*
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
*
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons
## v2.20.0 - 2024-06-05
### Fixed
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
* Added translations for choices of durations (> 5 hours)
### Feature
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
This endpoint should be added to make the endpoint works properly:
```yaml
security:
firewalls:
dav:
pattern: ^/dav
provider: chain_provider
stateless: true
guard:
authenticators:
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
```
## v2.19.0 - 2024-05-14 ## v2.19.0 - 2024-05-14
### Feature ### Feature
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side * ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side

View File

@@ -19,6 +19,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0", "doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0", "knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12", "knplabs/knp-time-bundle": "^1.12",
"knpuniversity/oauth2-client-bundle": "^2.10", "knpuniversity/oauth2-client-bundle": "^2.10",
@@ -31,6 +32,7 @@
"phpoffice/phpspreadsheet": "^1.16", "phpoffice/phpspreadsheet": "^1.16",
"ramsey/uuid-doctrine": "^1.7", "ramsey/uuid-doctrine": "^1.7",
"sensio/framework-extra-bundle": "^5.5", "sensio/framework-extra-bundle": "^5.5",
"smalot/pdfparser": "^2.10",
"spomky-labs/base64url": "^2.0", "spomky-labs/base64url": "^2.0",
"symfony/asset": "^5.4", "symfony/asset": "^5.4",
"symfony/browser-kit": "^5.4", "symfony/browser-kit": "^5.4",
@@ -91,12 +93,11 @@
"phpstan/phpstan": "^1.9", "phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-strict-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^10.5.24", "phpunit/phpunit": ">= 7.5",
"rector/rector": "^1.1.0", "rector/rector": "^1.1.0",
"symfony/debug-bundle": "^5.4", "symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4", "symfony/dotenv": "^5.4",
"symfony/maker-bundle": "^1.20", "symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^7.1",
"symfony/runtime": "^5.4", "symfony/runtime": "^5.4",
"symfony/stopwatch": "^5.4", "symfony/stopwatch": "^5.4",
"symfony/var-dumper": "^5.4" "symfony/var-dumper": "^5.4"
@@ -114,8 +115,6 @@
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle", "Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle", "Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle", "Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
"Chill\\FranceTravailApiBundle\\": "src/Bundle/ChillFranceTravailApiBundle/src",
"Chill\\JobBundle\\": "src/Bundle/ChillJobBundle/src",
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle", "Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle", "Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle", "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",

View File

@@ -21,7 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface
{ {
// add specific role for this filter // add specific role for this filter
public function addRole(): ?string public function addRole()
{ {
// we do not need any new role for this filter, so we return null // we do not need any new role for this filter, so we return null
return null; return null;

View File

@@ -56,7 +56,7 @@ We strongly encourage you to initialize a git repository at this step, to track
cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json
# install chill and some dependencies # install chill and some dependencies
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released # TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released
symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev symfony composer require chill-project/chill-bundles v3.0.0-alpha1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database. We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database.
You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually). You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually).
@@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine.
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_ in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
or in the :code:`.env.local` file, which should not be committed to the git repository. or in the :code:`.env.local` file, which should not be commited to the git repository.
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
by the symfony server, from the docker compose services. by the symfony server, from the docker compose services.
@@ -110,15 +110,10 @@ you can either:
.. code-block:: env .. code-block:: env
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm
# note: if you copy-paste the line above, the password will be "admin".
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
not the password in clear text). not the password in clear text).
- set up the jwt authentication bundle
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file.
Prepare migrations and other tools Prepare migrations and other tools
********************************** **********************************
@@ -135,8 +130,6 @@ To continue the installation process, you will have to run migrations:
symfony console messenger:setup-transports symfony console messenger:setup-transports
# prepare some views # prepare some views
symfony console chill:db:sync-views symfony console chill:db:sync-views
# generate jwt token, required for some api features (webdav access, ...)
symfony console lexik:jwt:generate-keypair
.. warning:: .. warning::
@@ -171,7 +164,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t
You will require some bundles to have the following development tools: You will require some bundles to have the following development tools:
- add fixtures - add fixtures
- add profiler and debug bundle - add profiler and var-dumper to debug
Install fixtures Install fixtures
**************** ****************
@@ -186,7 +179,7 @@ Install fixtures
This will generate user accounts, centers, and some basic configuration. This will generate user accounts, centers, and some basic configuration.
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`. visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
The password is always :code:`password`. The password is always :code:`password`.
@@ -199,7 +192,7 @@ Add web profiler and debugger
.. code-block:: bash .. code-block:: bash
symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
Working on chill bundles Working on chill bundles
************************ ************************

View File

@@ -27,7 +27,7 @@
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss-loader": "^7.0.2", "postcss-loader": "^7.0.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass-loader": "^14.0.0", "sass-loader": "^13.0.0",
"select2": "^4.0.13", "select2": "^4.0.13",
"select2-bootstrap-theme": "0.1.0-beta.10", "select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
@@ -46,11 +46,9 @@
"@fullcalendar/vue3": "^6.1.4", "@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/dompurify": "^3.0.5",
"dropzone": "^5.7.6", "dropzone": "^5.7.6",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"marked": "^12.0.2",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"mime": "^4.0.0", "mime": "^4.0.0",
"swagger-ui": "^4.15.5", "swagger-ui": "^4.15.5",
@@ -58,7 +56,7 @@
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2", "vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2", "vue-toast-notification": "^2.0",
"vuex": "^4.0.0" "vuex": "^4.0.0"
}, },
"browserslist": [ "browserslist": [

View File

@@ -1,29 +1,34 @@
parameters: parameters:
ignoreErrors: ignoreErrors:
-
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
- -
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1 count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
- -
message: "#^Property Chill\\\\CustomFieldsBundle\\\\Entity\\\\CustomField\\:\\:\\$required \\(false\\) does not accept bool\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1 count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php path: src/Bundle/ChillPersonBundle/Form/PersonType.php
- -
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2 count: 1
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
- -
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomFieldsGroup.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2 count: 2
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
-
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
- -
message: "#^Variable \\$participation might not be defined\\.$#" message: "#^Variable \\$participation might not be defined\\.$#"
@@ -35,106 +40,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
-
message: "#^Comparison operation \"\\>\" between \\(bool\\|int\\|Redis\\) and 0 results in an error\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
-
message: "#^Variable \\$response might not be defined\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
-
message: "#^Function GuzzleHttp\\\\Psr7\\\\get not found\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Function GuzzleHttp\\\\Psr7\\\\str not found\\.$#"
count: 2
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Parameter \\#1 \\$seconds of function sleep expects int, string given\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Controller/CSPersonController.php
-
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
-
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
-
message: "#^Property Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Metier\\:\\:\\$appellations is never read, only written\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Rome/Metier.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListCSPerson\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
-
message: "#^Variable \\$f might not be defined\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListFrein\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListFrein.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListProjetProfessionnel\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php
-
message: "#^Property Chill\\\\JobBundle\\\\Form\\\\ChoiceLoader\\\\RomeAppellationChoiceLoader\\:\\:\\$appellationRepository \\(Chill\\\\JobBundle\\\\Repository\\\\Rome\\\\AppellationRepository\\) does not accept Doctrine\\\\ORM\\\\EntityRepository\\<Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Appellation\\>\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Result of && is always false\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Strict comparison using \\=\\=\\= between array\\{\\} and Symfony\\\\Component\\\\Validator\\\\ConstraintViolationListInterface will always evaluate to false\\.$#"
count: 2
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Variable \\$metier might not be defined\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
-
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
- -
message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#" message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#"
count: 1 count: 1
@@ -160,31 +65,11 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
-
message: "#^Parameter \\#1 \\$user of method Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:\\:userHasAccessForCenter\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
- -
message: "#^Foreach overwrites \\$value with its value variable\\.$#" message: "#^Foreach overwrites \\$value with its value variable\\.$#"
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
- -
message: "#^Foreach overwrites \\$value with its value variable\\.$#" message: "#^Foreach overwrites \\$value with its value variable\\.$#"
count: 1 count: 1

View File

@@ -28,9 +28,6 @@ return static function (RectorConfig $rectorConfig): void {
// register a single rule // register a single rule
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);
// part of the symfony 54 rules // part of the symfony 54 rules
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); $rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
@@ -39,14 +36,14 @@ return static function (RectorConfig $rectorConfig): void {
//define sets of rules //define sets of rules
$rectorConfig->sets([ $rectorConfig->sets([
LevelSetList::UP_TO_PHP_82, \Rector\Symfony\Set\SymfonySetList::SYMFONY_50,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40, \Rector\Symfony\Set\SymfonySetList::SYMFONY_50_TYPES,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41, \Rector\Symfony\Set\SymfonySetList::SYMFONY_51,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42, \Rector\Symfony\Set\SymfonySetList::SYMFONY_52,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43, \Rector\Symfony\Set\SymfonySetList::SYMFONY_53,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44, \Rector\Symfony\Set\SymfonySetList::SYMFONY_54,
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY, \Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90, \Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
]); ]);
$rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [ $rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [

View File

@@ -99,10 +99,10 @@ final class ActivityController extends AbstractController
$form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod); $form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
$this->logger->notice('An activity has been removed', [ $this->logger->notice('An activity has been removed', [
'by_user' => $this->getUser()->getUsername(), 'by_user' => $this->getUser()->getUsername(),
'activity_id' => $activity->getId(), 'activity_id' => $activity->getId(),
@@ -640,6 +640,7 @@ final class ActivityController extends AbstractController
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_activity_activity_delete', $params)) ->setAction($this->generateUrl('chill_activity_activity_delete', $params))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -79,9 +79,11 @@ class ActivityReason
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return ActivityReason * @return ActivityReason
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -108,9 +110,11 @@ class ActivityReason
/** /**
* Set name. * Set name.
* *
* @param array $name
*
* @return ActivityReason * @return ActivityReason
*/ */
public function setName(array $name) public function setName($name)
{ {
$this->name = $name; $this->name = $name;

View File

@@ -152,7 +152,7 @@ class ListActivityHelper
return ''; return '';
} }
return $this->translator->trans((string) $value); return $this->translator->trans($value);
}, },
}; };
} }

View File

@@ -73,7 +73,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt
$qb->andWhere( $qb->andWhere(
$qb->expr()->exists( $qb->expr()->exists(
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod" 'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
) )
); );

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new activity in accompanying course', [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
// 'activityType_id' => '',
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 10,
'icon' => 'plus',
])
;
}
}
}

View File

@@ -15,9 +15,9 @@ use Chill\ActivityBundle\Entity\ActivityType;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ActivityTypeRepository implements ActivityTypeRepositoryInterface final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em)
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ abstract class AbstractElementController extends AbstractController
$indexPage = 'chill_budget_elements_household_index'; $indexPage = 'chill_budget_elements_household_index';
} }
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
@@ -198,9 +198,10 @@ abstract class AbstractElementController extends AbstractController
/** /**
* Creates a form to delete a help request entity by id. * Creates a form to delete a help request entity by id.
*/ */
private function createDeleteForm(): \Symfony\Component\Form\FormInterface private function createDeleteForm(): Form
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setMethod(Request::METHOD_DELETE)
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -100,7 +100,7 @@ class Charge extends AbstractElement implements HasCentersInterface
return $this; return $this;
} }
public function setHelp(?string $help) public function setHelp($help)
{ {
$this->help = $help; $this->help = $help;

View File

@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ChargeKind;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ChargeKindRepository implements ChargeKindRepositoryInterface final class ChargeKindRepository implements ChargeKindRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {

View File

@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ResourceKind;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ResourceKindRepository implements ResourceKindRepositoryInterface final class ResourceKindRepository implements ResourceKindRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {

View File

@@ -84,7 +84,7 @@ class CalendarController extends AbstractController
$form = $this->createDeleteForm($entity); $form = $this->createDeleteForm($entity);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isValid()) { if ($form->isValid()) {
@@ -512,6 +512,7 @@ class CalendarController extends AbstractController
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()])) ->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -42,7 +42,7 @@ class RemoteCalendarConnectAzureController
->redirect(['https://graph.microsoft.com/.default', 'offline_access'], []); ->redirect(['https://graph.microsoft.com/.default', 'offline_access'], []);
} }
#[Route(path: '/connect/azure/check', name: 'chill_calendar_remote_connect_azure_check', schemes: 'https')] #[Route(path: '/connect/azure/check', name: 'chill_calendar_remote_connect_azure_check')]
public function connectAzureCheck(Request $request): Response public function connectAzureCheck(Request $request): Response
{ {
/** @var Azure $client */ /** @var Azure $client */

View File

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

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Menu;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new calendar in accompanying course', [
'route' => 'chill_calendar_calendar_new',
'routeParameters' => [
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 20,
'icon' => 'plus',
])
;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -172,9 +172,11 @@ class CustomField
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return CustomField * @return CustomField
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -222,16 +224,18 @@ class CustomField
/** /**
* Set order. * Set order.
* *
* @param float $order
*
* @return CustomField * @return CustomField
*/ */
public function setOrdering(?float $order) public function setOrdering($order)
{ {
$this->ordering = $order; $this->ordering = $order;
return $this; return $this;
} }
public function setRequired(bool $required) public function setRequired($required)
{ {
$this->required = $required; $this->required = $required;
@@ -241,7 +245,7 @@ class CustomField
/** /**
* @return $this * @return $this
*/ */
public function setSlug(?string $slug) public function setSlug($slug)
{ {
$this->slug = $slug; $this->slug = $slug;
@@ -251,9 +255,11 @@ class CustomField
/** /**
* Set type. * Set type.
* *
* @param string $type
*
* @return CustomField * @return CustomField
*/ */
public function setType(?string $type) public function setType($type)
{ {
$this->type = $type; $this->type = $type;

View File

@@ -129,7 +129,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -139,7 +139,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setInternalKey(string $internal_key) public function setInternalKey($internal_key)
{ {
$this->internalKey = $internal_key; $this->internalKey = $internal_key;
@@ -149,7 +149,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setKey(?string $key) public function setKey($key)
{ {
$this->key = $key; $this->key = $key;

View File

@@ -69,7 +69,7 @@ class CustomFieldsDefaultGroup
* *
* @return CustomFieldsDefaultGroup * @return CustomFieldsDefaultGroup
*/ */
public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldsGroup) public function setCustomFieldsGroup($customFieldsGroup)
{ {
$this->customFieldsGroup = $customFieldsGroup; $this->customFieldsGroup = $customFieldsGroup;
@@ -79,9 +79,11 @@ class CustomFieldsDefaultGroup
/** /**
* Set entity. * Set entity.
* *
* @param string $entity
*
* @return CustomFieldsDefaultGroup * @return CustomFieldsDefaultGroup
*/ */
public function setEntity(?string $entity) public function setEntity($entity)
{ {
$this->entity = $entity; $this->entity = $entity;

View File

@@ -165,9 +165,11 @@ class CustomFieldsGroup
/** /**
* Set entity. * Set entity.
* *
* @param string $entity
*
* @return CustomFieldsGroup * @return CustomFieldsGroup
*/ */
public function setEntity(?string $entity) public function setEntity($entity)
{ {
$this->entity = $entity; $this->entity = $entity;

View File

@@ -21,14 +21,14 @@ use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class RelatorioDriver implements DriverInterface final class RelatorioDriver implements DriverInterface
{ {
private string $url; private readonly string $url;
public function __construct( public function __construct(
private HttpClientInterface $client, private readonly HttpClientInterface $client,
ParameterBagInterface $parameterBag, ParameterBagInterface $parameterBag,
private LoggerInterface $logger private readonly LoggerInterface $logger
) { ) {
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url']; $this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
} }

View File

@@ -16,11 +16,11 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
final readonly class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager, private RequestStack $requestStack) public function __construct(EntityManagerInterface $entityManager, private readonly RequestStack $requestStack)
{ {
$this->repository = $entityManager->getRepository(DocGeneratorTemplate::class); $this->repository = $entityManager->getRepository(DocGeneratorTemplate::class);
} }

View File

@@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @template T of object
*/
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
{
public function testNullValueHasSameKeysAsNull(): void
{
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
foreach ($normalizedObject as $key => $value) {
if (in_array($key, ['isNull', 'type'])) {
continue;
}
if (is_array($value)) {
if (array_is_list($value)) {
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
} else {
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
}
} elseif (is_string($value)) {
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
}
}
}
/**
* @return T
*/
abstract public function provideNotNullObject(): object;
/**
* @return class-string<T>
*/
abstract public function provideDocGenExpectClass(): string;
abstract public function getNormalizer(): NormalizerInterface;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
class SignatureRequestController
{
public function __construct(
private MessageBusInterface $messageBus,
private StoredObjectManagerInterface $storedObjectManager,
) {}
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
public function processSignature(StoredObject $storedObject): Response
{
$content = $this->storedObjectManager->read($storedObject);
$this->messageBus->dispatch(new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'test signature',
$content
));
return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
}
}

View File

@@ -129,7 +129,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
return $this; return $this;
} }
public function setUser(?\Chill\MainBundle\Entity\User $user): self public function setUser($user): self
{ {
$this->user = $user; $this->user = $user;

View File

@@ -86,7 +86,7 @@ class DocumentCategory
return $this; return $this;
} }
public function setDocumentClass(?string $documentClass): self public function setDocumentClass($documentClass): self
{ {
$this->documentClass = $documentClass; $this->documentClass = $documentClass;

View File

@@ -55,14 +55,14 @@ class PersonDocument extends Document implements HasCenterInterface, HasScopeInt
return $this->scope; return $this->scope;
} }
public function setPerson(Person $person): self public function setPerson($person): self
{ {
$this->person = $person; $this->person = $person;
return $this; return $this;
} }
public function setScope(?Scope $scope): self public function setScope($scope): self
{ {
$this->scope = $scope; $this->scope = $scope;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
final readonly class RequestPdfSignMessage
{
public function __construct(
public int $signatureId,
public PDFSignatureZone $PDFSignatureZone,
public int $signatureZoneIndex,
public string $reason,
public string $content,
) {}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
{
public function __construct(
private NormalizerInterface $normalizer,
private DenormalizerInterface $denormalizer,
) {}
public function decode(array $encodedEnvelope): Envelope
{
$body = $encodedEnvelope['body'];
$headers = $encodedEnvelope['headers'];
if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
throw new MessageDecodingFailedException('serializer does not support this message');
}
$data = json_decode($body, true);
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
AbstractNormalizer::GROUPS => ['write'],
]);
$message = new RequestPdfSignMessage(
$data['signatureId'],
$zoneSignature,
$data['signatureZoneIndex'],
$data['reason'],
base64_decode($data['content']),
);
// in case of redelivery, unserialize any stamps
$stamps = [];
if (isset($headers['stamps'])) {
$stamps = unserialize($headers['stamps']);
}
return new Envelope($message, $stamps);
}
public function encode(Envelope $envelope): array
{
$message = $envelope->getMessage();
if (!$message instanceof RequestPdfSignMessage) {
throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
}
$data = [
'signatureId' => $message->signatureId,
'signatureZoneIndex' => $message->signatureZoneIndex,
'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
'reason' => $message->reason,
'content' => base64_encode($message->content),
];
$allStamps = [];
foreach ($envelope->all() as $stamp) {
if ($stamp instanceof NonSendableStampInterface) {
continue;
}
$allStamps = [...$allStamps, ...$stamp];
}
return [
'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
'headers' => [
'stamps' => serialize($allStamps),
'Message' => RequestPdfSignMessage::class,
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service\Signature;
use Symfony\Component\Serializer\Annotation\Groups;
final readonly class PDFPage
{
public function __construct(
#[Groups(['read'])]
public int $index,
#[Groups(['read'])]
public float $width,
#[Groups(['read'])]
public float $height,
) {}
public function equals(self $page): bool
{
return $page->index === $this->index
&& round($page->width, 2) === round($this->width, 2)
&& round($page->height, 2) === round($this->height, 2);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service\Signature;
use Symfony\Component\Serializer\Annotation\Groups;
final readonly class PDFSignatureZone
{
public function __construct(
#[Groups(['read'])]
public float $x,
#[Groups(['read'])]
public float $y,
#[Groups(['read'])]
public float $height,
#[Groups(['read'])]
public float $width,
#[Groups(['read'])]
public PDFPage $PDFPage,
) {}
public function equals(self $other): bool
{
return
$this->x == $other->x
&& $this->y == $other->y
&& $this->height == $other->height
&& $this->width == $other->width
&& $this->PDFPage->equals($other->PDFPage);
}
}

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\DocStoreBundle\Service\Signature;
use Smalot\PdfParser\Parser;
class PDFSignatureZoneParser
{
public const ZONE_SIGNATURE_START = 'signature_zone';
private Parser $parser;
public function __construct(
public float $defaultHeight = 180.0,
public float $defaultWidth = 180.0,
) {
$this->parser = new Parser();
}
/**
* @return list<PDFSignatureZone>
*/
public function findSignatureZones(string $fileContent): array
{
$pdf = $this->parser->parseContent($fileContent);
$zones = [];
$defaults = $pdf->getObjectsByType('Pages');
$defaultPage = reset($defaults);
$defaultPageDetails = $defaultPage->getDetails();
foreach ($pdf->getPages() as $index => $page) {
$details = $page->getDetails();
$pdfPage = new PDFPage(
$index,
(float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
(float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
);
foreach ($page->getDataTm() as $dataTm) {
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
$zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
}
}
}
return $zones;
}
}

View File

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Entity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectTest extends KernelTestCase
{
public function testSaveHistory(): void
{
$storedObject = new StoredObject();
$storedObject
->setFilename('test_0')
->setIv([2, 4, 6, 8])
->setKeyInfos(['key' => ['data0' => 'data0']])
->setType('text/html');
$storedObject->saveHistory();
$storedObject
->setFilename('test_1')
->setIv([8, 10, 12])
->setKeyInfos(['key' => ['data1' => 'data1']])
->setType('text/text');
$storedObject->saveHistory();
self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']);
self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']);
self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']);
self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']);
self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']);
self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']);
self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']);
self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']);
}
}

View File

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

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @internal
*
* @coversNothing
*/
class RequestPdfSignMessageSerializerTest extends TestCase
{
public function testEncode(): void
{
$serializer = $this->buildSerializer();
$envelope = new Envelope(
$request = new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'metadata to add to the signature',
'abc'
),
);
$actual = $serializer->encode($envelope);
$expectedBody = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'content' => base64_encode($request->content),
]);
self::assertIsArray($actual);
self::assertArrayHasKey('body', $actual);
self::assertArrayHasKey('headers', $actual);
self::assertEquals($expectedBody, $actual['body']);
}
public function testDecode(): void
{
$serializer = $this->buildSerializer();
$request = new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'metadata to add to the signature',
'abc'
);
$bodyAsString = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'content' => base64_encode($request->content),
], JSON_THROW_ON_ERROR);
$actual = $serializer->decode([
'body' => $bodyAsString,
'headers' => [
'Message' => RequestPdfSignMessage::class,
],
]);
self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
self::assertEquals($request->reason, $actual->getMessage()->reason);
self::assertEquals($request->content, $actual->getMessage()->content);
self::assertNotNull($actual->getMessage()->PDFSignatureZone);
}
private function buildSerializer(): RequestPdfSignMessageSerializer
{
$normalizer =
new class () implements NormalizerInterface {
public function normalize($object, ?string $format = null, array $context = []): array
{
if (!$object instanceof PDFSignatureZone) {
throw new UnexpectedValueException('expected RequestPdfSignMessage');
}
return [
'x' => $object->x,
];
}
public function supportsNormalization($data, ?string $format = null): bool
{
return $data instanceof PDFSignatureZone;
}
};
$denormalizer = new class () implements DenormalizerInterface {
public function denormalize($data, string $type, ?string $format = null, array $context = [])
{
return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
}
public function supportsDenormalization($data, string $type, ?string $format = null)
{
return PDFSignatureZone::class === $type;
}
};
$serializer = new Serializer([$normalizer, $denormalizer]);
return new RequestPdfSignMessageSerializer($serializer, $serializer);
}
}

View File

@@ -0,0 +1,77 @@
<?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 Tests\Service\Signature;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use PHPUnit\Framework\TestCase;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
/**
* @internal
*
* @coversNothing
*/
class PDFSignatureZoneParserTest extends TestCase
{
private static PDFSignatureZoneParser $parser;
public static function setUpBeforeClass(): void
{
self::$parser = new PDFSignatureZoneParser();
}
/**
* @dataProvider provideFiles
*
* @param list<PDFSignatureZone> $expected
*/
public function testFindSignatureZones(string $filePath, array $expected): void
{
$content = file_get_contents($filePath);
if (false === $content) {
throw new \LogicException("Unable to read file {$filePath}");
}
$actual = self::$parser->findSignatureZones($content);
self::assertEquals(count($expected), count($actual));
foreach ($actual as $index => $signatureZone) {
self::assertObjectEquals($expected[$index], $signatureZone);
}
}
public static function provideFiles(): iterable
{
yield [
__DIR__.'/data/signature_2_signature_page_1.pdf',
[
new PDFSignatureZone(
127.7,
95.289,
180.0,
180.0,
$page = new PDFPage(0, 595.30393, 841.8897)
),
new PDFSignatureZone(
269.5,
95.289,
180.0,
180.0,
$page,
),
],
];
}
}

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\EventType; use Chill\EventBundle\Form\EventType;
use Chill\EventBundle\Form\Type\PickEventType; use Chill\EventBundle\Form\Type\PickEventType;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
@@ -61,7 +61,7 @@ final class EventController extends AbstractController
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
) {} ) {}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'DELETE'])]
public function deleteAction($event_id, Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|Response public function deleteAction($event_id, Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|Response
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -78,10 +78,10 @@ final class EventController extends AbstractController
$form = $this->createDeleteForm($event_id); $form = $this->createDeleteForm($event_id);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
foreach ($participations as $participation) { foreach ($participations as $participation) {
$em->remove($participation); $em->remove($participation);
} }
@@ -108,6 +108,28 @@ final class EventController extends AbstractController
]); ]);
} }
/**
* Displays a form to edit an existing Event entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit')]
public function editAction($event_id): Response
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Event::class)->find($event_id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Event entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillEvent/Event/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/** /**
* List events subscriptions for a person. * List events subscriptions for a person.
* *
@@ -291,7 +313,7 @@ final class EventController extends AbstractController
/** /**
* Edits an existing Event entity. * Edits an existing Event entity.
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit', methods: ['GET', 'POST', 'PUT'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/update', name: 'chill_event__event_update', methods: ['POST', 'PUT'])]
public function updateAction(Request $request, $event_id): \Symfony\Component\HttpFoundation\RedirectResponse|Response public function updateAction(Request $request, $event_id): \Symfony\Component\HttpFoundation\RedirectResponse|Response
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -302,20 +324,14 @@ final class EventController extends AbstractController
throw $this->createNotFoundException('Unable to find Event entity.'); throw $this->createNotFoundException('Unable to find Event entity.');
} }
$editForm = $this->createForm(EventType::class, $entity, [ $editForm = $this->createEditForm($entity);
'center' => $entity->getCenter(),
'role' => EventVoter::UPDATE,
]);
$editForm->add('submit', SubmitType::class, ['label' => 'Update']);
$editForm->handleRequest($request); $editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isValid()) {
$em->persist($entity);
$em->flush(); $em->flush();
$this->addFlash('success', $this->translator->trans('The event was updated')); $this->addFlash('success', $this->translator
->trans('The event was updated'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]); return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
} }
@@ -402,6 +418,7 @@ final class EventController extends AbstractController
$builder->add('event_id', HiddenType::class, [ $builder->add('event_id', HiddenType::class, [
'data' => $event->getId(), 'data' => $event->getId(),
]); ]);
dump($event->getId());
return $builder->getForm(); return $builder->getForm();
} }
@@ -583,7 +600,29 @@ final class EventController extends AbstractController
->setAction($this->generateUrl('chill_event__event_delete', [ ->setAction($this->generateUrl('chill_event__event_delete', [
'event_id' => $event_id, 'event_id' => $event_id,
])) ]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }
/**
* Creates a form to edit a Event entity.
*
* @return \Symfony\Component\Form\FormInterface
*/
private function createEditForm(Event $entity)
{
$form = $this->createForm(EventType::class, $entity, [
'action' => $this->generateUrl('chill_event__event_update', ['event_id' => $entity->getId()]),
'method' => 'PUT',
'center' => $entity->getCenter(),
'role' => 'CHILL_EVENT_CREATE',
]);
$form->remove('center');
$form->add('submit', SubmitType::class, ['label' => 'Update']);
return $form;
}
} }

View File

@@ -201,7 +201,7 @@ class EventTypeController extends AbstractController
/** /**
* Creates a form to delete a EventType entity by id. * Creates a form to delete a EventType entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {
@@ -210,6 +210,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_delete', 'chill_eventtype_admin_delete',
['id' => $id] ['id' => $id]
)) ))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType; use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Repository\EventRepository; use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
@@ -259,10 +259,10 @@ final class ParticipationController extends AbstractController
$form = $this->createDeleteForm($participation_id); $form = $this->createDeleteForm($participation_id);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
$em->remove($participation); $em->remove($participation);
$em->flush(); $em->flush();
@@ -753,6 +753,7 @@ final class ParticipationController extends AbstractController
->setAction($this->generateUrl('chill_event_participation_delete', [ ->setAction($this->generateUrl('chill_event_participation_delete', [
'participation_id' => $participation_id, 'participation_id' => $participation_id,
])) ]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -201,7 +201,7 @@ class RoleController extends AbstractController
/** /**
* Creates a form to delete a Role entity by id. * Creates a form to delete a Role entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {

View File

@@ -201,12 +201,13 @@ class StatusController extends AbstractController
/** /**
* Creates a form to delete a Status entity by id. * Creates a form to delete a Status entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id])) ->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection; namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -33,13 +33,12 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml'); $loader->load('services.yaml');
$loader->load('services/security.yaml'); $loader->load('services/authorization.yaml');
$loader->load('services/fixtures.yaml'); $loader->load('services/fixtures.yaml');
$loader->load('services/forms.yaml'); $loader->load('services/forms.yaml');
$loader->load('services/repositories.yaml'); $loader->load('services/repositories.yaml');
$loader->load('services/search.yaml'); $loader->load('services/search.yaml');
$loader->load('services/timeline.yaml'); $loader->load('services/timeline.yaml');
$loader->load('services/export.yaml');
} }
/** (non-PHPdoc). /** (non-PHPdoc).

View File

@@ -47,7 +47,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
private ?Scope $circle = null; private ?Scope $circle = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)]
private ?\DateTime $date = null; private ?\DateTime $date;
#[ORM\Id] #[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
@@ -265,9 +265,11 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* Set label. * Set label.
* *
* @param string $label
*
* @return Event * @return Event
*/ */
public function setName(?string $label) public function setName($label)
{ {
$this->name = $label; $this->name = $label;

View File

@@ -146,9 +146,11 @@ class EventType
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return EventType * @return EventType
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

View File

@@ -81,9 +81,11 @@ class Role
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return Role * @return Role
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

View File

@@ -81,9 +81,11 @@ class Status
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return Status * @return Status
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

View File

@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class EventDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by month' => 'month',
'by week' => 'week',
'by year' => 'year',
];
private const DEFAULT_CHOICE = 'year';
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$order = null;
switch ($data['frequency']) {
case 'month':
$fmt = 'YYYY-MM';
break;
case 'week':
$fmt = 'YYYY-IW';
break;
case 'year':
$fmt = 'YYYY';
$order = 'DESC';
break;
default:
throw new \RuntimeException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
}
$qb->addSelect(sprintf("TO_CHAR(event.date, '%s') AS date_aggregator", $fmt));
$qb->addGroupBy('date_aggregator');
$qb->addOrderBy('date_aggregator', $order);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('frequency', ChoiceType::class, [
'choices' => self::CHOICES,
'multiple' => false,
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return ['frequency' => self::DEFAULT_CHOICE];
}
public function getLabels($key, array $values, $data)
{
return static function ($value) use ($data): string {
if ('_header' === $value) {
return 'by '.$data['frequency'];
}
if (null === $value) {
return '';
}
return match ($data['frequency']) {
default => $value,
};
};
}
public function getQueryKeys($data): array
{
return ['date_aggregator'];
}
public function getTitle(): string
{
return 'Group event by date';
}
}

View File

@@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class EventTypeAggregator implements AggregatorInterface
{
final public const KEY = 'event_type_aggregator';
public function __construct(protected EventTypeRepository $eventTypeRepository, protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('eventtype', $qb->getAllAliases(), true)) {
$qb->leftJoin('event.type', 'eventtype');
}
$qb->addSelect(sprintf('IDENTITY(event.type) AS %s', self::KEY));
$qb->addGroupBy(self::KEY);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): \Closure
{
return function (int|string|null $value): string {
if ('_header' === $value) {
return 'Event type';
}
if (null === $value || '' === $value || null === $t = $this->eventTypeRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($t->getName());
};
}
public function getQueryKeys($data): array
{
return [self::KEY];
}
public function getTitle()
{
return 'Group by event type';
}
}

View File

@@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class RoleAggregator implements AggregatorInterface
{
final public const KEY = 'part_role_aggregator';
public function __construct(protected RoleRepository $roleRepository, protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('event_part', $qb->getAllAliases(), true)) {
$qb->leftJoin('event_part.role', 'role');
}
$qb->addSelect(sprintf('IDENTITY(event_part.role) AS %s', self::KEY));
$qb->addGroupBy(self::KEY);
}
public function applyOn(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function buildForm(FormBuilderInterface $builder)
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): \Closure
{
return function (int|string|null $value): string {
if ('_header' === $value) {
return 'Participant role';
}
if (null === $value || '' === $value || null === $r = $this->roleRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($r->getName());
};
}
public function getQueryKeys($data): array
{
return [self::KEY];
}
public function getTitle()
{
return 'Group by participant role';
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export;
/**
* This class declare constants used for the export framework.
*/
abstract class Declarations
{
final public const EVENT = 'event';
final public const EVENT_PARTICIPANTS = 'event_participants';
}

View File

@@ -1,125 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\ParticipationRepository;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEventParticipations implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private ParticipationRepository $participationRepository,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
public function buildForm(FormBuilderInterface $builder) {}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_TABULAR];
}
public function getDescription()
{
return 'Count participants to an event by various parameters.';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
if ('export_count_event_participants' !== $key) {
throw new \LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Count event participants' : $value;
}
public function getQueryKeys($data)
{
return ['export_count_event_participants'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'Count event participants';
}
public function getType(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->participationRepository
->createQueryBuilder('event_part')
->join('event_part.person', 'person');
$qb->select('COUNT(event_part.id) as export_count_event_participants');
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
return $qb;
}
public function requiredRole(): string
{
return ParticipationVoter::STATS;
}
public function supportsModifiers()
{
return [
Declarations::EVENT_PARTICIPANTS,
PersonDeclarations::PERSON_TYPE,
];
}
}

View File

@@ -1,126 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\EventBundle\Export\Declarations;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEvents implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private EventRepository $eventRepository,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
public function buildForm(FormBuilderInterface $builder) {}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_TABULAR];
}
public function getDescription()
{
return 'Count events by various parameters.';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
if ('export_count_event' !== $key) {
throw new \LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Number of events' : $value;
}
public function getQueryKeys($data)
{
return ['export_count_event'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'Count events';
}
public function getType(): string
{
return Declarations::EVENT;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->eventRepository
->createQueryBuilder('event')
->leftJoin('event.participations', 'epart')
->leftJoin('epart.person', 'person');
$qb->select('COUNT(DISTINCT event.id) as export_count_event');
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
return $qb;
}
public function requiredRole(): string
{
return EventVoter::STATS;
}
public function supportsModifiers()
{
return [
Declarations::EVENT,
PersonDeclarations::PERSON_TYPE,
];
}
}

View File

@@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventDateFilter implements FilterInterface
{
public function __construct(protected TranslatorInterface $translator, private readonly RollingDateConverterInterface $rollingDateConverter) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->between(
'event.date',
':date_from',
':date_to'
);
if ($where instanceof Expr\Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter(
'date_from',
$this->rollingDateConverter->convert($data['date_from'])
);
$qb->setParameter(
'date_to',
$this->rollingDateConverter->convert($data['date_to'])
);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', PickRollingDateType::class, [
'label' => 'Events after this date',
])
->add('date_to', PickRollingDateType::class, [
'label' => 'Events before this date',
]);
}
public function getFormDefaultData(): array
{
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by date of event: only between %date_from% and %date_to%',
[
'%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
],
];
}
public function getTitle()
{
return 'Filtered by event date';
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class EventTypeFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected EventTypeRepository $eventTypeRepository
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('event.type', ':selected_event_types');
$qb->andWhere($clause);
$qb->setParameter('selected_event_types', $data['types']);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('types', EntityType::class, [
'choices' => $this->eventTypeRepository->findAllActive(),
'class' => EventType::class,
'choice_label' => fn (EventType $ety) => $this->translatableStringHelper->localize($ety->getName()),
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{
$typeNames = array_map(
fn (EventType $t): string => $this->translatableStringHelper->localize($t->getName()),
$this->eventTypeRepository->findBy(['id' => $data['types'] instanceof \Doctrine\Common\Collections\Collection ? $data['types']->toArray() : $data['types']])
);
return ['Filtered by event type: only %list%', [
'%list%' => implode(', ', $typeNames),
]];
}
public function getTitle()
{
return 'Filtered by event type';
}
public function validateForm($data, ExecutionContextInterface $context)
{
if (null === $data['types'] || 0 === \count($data['types'])) {
$context
->buildViolation('At least one type must be chosen')
->addViolation();
}
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class RoleFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected RoleRepository $roleRepository
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('event_part.role', ':selected_part_roles');
$qb->andWhere($clause);
$qb->setParameter('selected_part_roles', $data['part_roles']);
}
public function applyOn(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('part_roles', EntityType::class, [
'choices' => $this->roleRepository->findAllActive(),
'class' => Role::class,
'choice_label' => fn (Role $r) => $this->translatableStringHelper->localize($r->getName()),
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{
$roleNames = array_map(
fn (Role $r): string => $this->translatableStringHelper->localize($r->getName()),
$this->roleRepository->findBy(['id' => $data['part_roles'] instanceof \Doctrine\Common\Collections\Collection ? $data['part_roles']->toArray() : $data['part_roles']])
);
return ['Filtered by participant roles: only %list%', [
'%list%' => implode(', ', $roleNames),
]];
}
public function getTitle()
{
return 'Filter by participant roles';
}
public function validateForm($data, ExecutionContextInterface $context)
{
if (null === $data['part_roles'] || 0 === \count($data['part_roles'])) {
$context
->buildViolation('At least one role must be chosen')
->addViolation();
}
}
}

View File

@@ -13,7 +13,6 @@ namespace Chill\EventBundle\Form;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Form\StoredObjectType; use Chill\DocStoreBundle\Form\StoredObjectType;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Form\Type\PickEventTypeType; use Chill\EventBundle\Form\Type\PickEventTypeType;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
@@ -24,7 +23,6 @@ use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Form\Type\ScopePickerType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -33,9 +31,7 @@ class EventType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
->add('name', TextType::class, [ ->add('name')
'required' => true,
])
->add('date', ChillDateTimeType::class, [ ->add('date', ChillDateTimeType::class, [
'required' => true, 'required' => true,
]) ])
@@ -79,7 +75,7 @@ class EventType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => Event::class, 'data_class' => \Chill\EventBundle\Entity\Event::class,
]); ]);
$resolver $resolver
->setRequired(['center', 'role']) ->setRequired(['center', 'role'])

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu; namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu; namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;

View File

@@ -13,7 +13,7 @@ namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;

View File

@@ -12,57 +12,13 @@ declare(strict_types=1);
namespace Chill\EventBundle\Repository; namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Role; use Chill\EventBundle\Entity\Role;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
readonly class RoleRepository implements ObjectRepository class RoleRepository extends ServiceEntityRepository
{ {
private EntityRepository $repository; public function __construct(ManagerRegistry $registry)
public function __construct(EntityManagerInterface $entityManager, private TranslatableStringHelper $translatableStringHelper)
{ {
$this->repository = $entityManager->getRepository(Role::class); parent::__construct($registry, Role::class);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function find($id)
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findAllActive(): array
{
$roles = $this->repository->findBy(['active' => true]);
usort($roles, fn (Role $a, Role $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName()));
return $roles;
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return Role::class;
} }
} }

View File

@@ -1,7 +1,7 @@
{% import '@ChillPerson/Person/macro.html.twig' as person_macro %} {% import '@ChillPerson/Person/macro.html.twig' as person_macro %}
{% if ignored_participations|length > 0 %} {% if ignored_participations|length > 0 %}
<p>{{ 'ignored_participations'|trans({'count': ignored_participations|length}) }}:</p> <p>{% transchoice ignored_participations|length %}The following people have been ignored because they are already participating on the event{% endtranschoice %}&nbsp;:</p>
<ul> <ul>
{% for p in ignored_participations %} {% for p in ignored_participations %}
<li>{{ person_macro.render(p.person) }}</li> <li>{{ person_macro.render(p.person) }}</li>

View File

@@ -9,19 +9,18 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
namespace Chill\EventBundle\Security; namespace Chill\EventBundle\Security\Authorization;
use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Event;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter; use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
/** /**
* Description of EventVoter. * Description of EventVoter.
@@ -43,46 +42,61 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
final public const UPDATE = 'CHILL_EVENT_UPDATE'; final public const UPDATE = 'CHILL_EVENT_UPDATE';
final public const STATS = 'CHILL_EVENT_STATS'; /**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
private readonly VoterHelperInterface $voterHelper; /**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
public function __construct( public function __construct(
private readonly AuthorizationHelper $authorizationHelper, AccessDecisionManagerInterface $accessDecisionManager,
private readonly LoggerInterface $logger, AuthorizationHelper $authorizationHelper,
VoterHelperFactoryInterface $voterHelperFactory LoggerInterface $logger
) { ) {
$this->voterHelper = $voterHelperFactory $this->accessDecisionManager = $accessDecisionManager;
->generate(self::class) $this->authorizationHelper = $authorizationHelper;
->addCheckFor(null, [self::SEE]) $this->logger = $logger;
->addCheckFor(Event::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
} }
public function getRoles(): array public function getRoles(): array
{ {
return [...self::ROLES, self::STATS]; return self::ROLES;
} }
public function getRolesWithHierarchy(): array public function getRolesWithHierarchy(): array
{ {
return [ return [
'Event' => $this->getRoles(), 'Event' => self::ROLES,
]; ];
} }
public function getRolesWithoutScope(): array public function getRolesWithoutScope(): array
{ {
return [self::ROLES, self::STATS]; return [];
} }
public function supports($attribute, $subject) public function supports($attribute, $subject)
{ {
return $this->voterHelper->supports($attribute, $subject); return ($subject instanceof Event && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
} }
/**
* @param string $attribute
* @param Event $subject
*
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{ {
$this->logger->debug(sprintf('Voting from %s class', self::class)); $this->logger->debug(sprintf('Voting from %s class', self::class));
@@ -104,5 +118,15 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
->getReachableCenters($token->getUser(), $attribute); ->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0; return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
} }
} }

View File

@@ -9,19 +9,18 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
namespace Chill\EventBundle\Security; namespace Chill\EventBundle\Security\Authorization;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter; use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{ {
@@ -40,48 +39,58 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE'; final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE';
final public const STATS = 'CHILL_EVENT_PARTICIPATION_STATS'; /**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
private readonly VoterHelperInterface $voterHelper; /**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
public function __construct( public function __construct(
private readonly AuthorizationHelper $authorizationHelper, AccessDecisionManagerInterface $accessDecisionManager,
private readonly LoggerInterface $logger, AuthorizationHelper $authorizationHelper,
VoterHelperFactoryInterface $voterHelperFactory LoggerInterface $logger
) { ) {
$this->voterHelper = $voterHelperFactory $this->accessDecisionManager = $accessDecisionManager;
->generate(self::class) $this->authorizationHelper = $authorizationHelper;
->addCheckFor(null, [self::SEE]) $this->logger = $logger;
->addCheckFor(Participation::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
} }
public function getRoles(): array public function getRoles(): array
{ {
return [...self::ROLES, self::STATS]; return self::ROLES;
} }
public function getRolesWithHierarchy(): array public function getRolesWithHierarchy(): array
{ {
return [ return [
'Participation' => $this->getRoles(), 'Event' => self::ROLES,
]; ];
} }
public function getRolesWithoutScope(): array public function getRolesWithoutScope(): array
{ {
return [self::ROLES, self::STATS]; return [];
} }
public function supports($attribute, $subject) public function supports($attribute, $subject)
{ {
return $this->voterHelper->supports($attribute, $subject); return ($subject instanceof Participation && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
} }
/** /**
* @param string $attribute * @param string $attribute
* @param Participation $subject
* *
* @return bool * @return bool
*/ */
@@ -106,5 +115,15 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
->getReachableCenters($token->getUser(), $attribute); ->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0; return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
} }
} }

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