Compare commits

..

34 Commits

Author SHA1 Message Date
d33dcacc46 Use better namespacing for configuring workflow signature documents 2024-07-18 16:09:34 +02:00
8d97df9f96 Remove trailing ParamConverter annotation 2024-07-18 15:17:35 +02:00
2822800c76 Changie added 2024-07-18 15:12:47 +02:00
8973b7c20b Move logic from twig template to controller and refactor workflow controller 2024-07-18 15:11:43 +02:00
7f144da1a7 Remove todo 2024-07-10 16:27:06 +02:00
ab4193938d Adjust the structure of the signature metadata 2024-07-10 15:39:38 +02:00
e2426ba1d8 Rename configuration parameter for document kinds 2024-07-10 15:38:51 +02:00
8209990437 Add todo as reminder to change isSignature logic in controller 2024-07-10 15:05:08 +02:00
b1885de3e2 Merge branch '288-signature-zone-workflow' of https://gitlab.com/Chill-Projet/chill-bundles into 288-signature-zone-workflow 2024-07-10 14:59:50 +02:00
218280304c php cs fixes 2024-07-10 12:49:51 +02:00
8a7b48b201 Implement logic to save metadata to signature 2024-07-10 12:49:05 +02:00
52a9aab73f Create form for adding document info in metadata property of signature 2024-07-10 12:48:48 +02:00
8f358112b1 Add configuration for signature document types 2024-07-10 12:48:03 +02:00
57a07af3db Display names and sign buttons for person or user signatures 2024-07-09 17:39:17 +02:00
fd216ff66e Remove metadata property from workflow entity 2024-07-09 15:47:06 +02:00
689c2c574a Correct translation 2024-07-09 10:05:15 +00:00
a8de18beac Layout template for adding signature zone to workflow 2024-07-04 17:12:32 +02:00
babca5fc0f Add metadata property to workflow to allow adding info about signatures needed 2024-07-04 17:11:57 +02:00
f2c5663b05 Adjust and add necessary twig templates for signature zone 2024-07-04 09:23:58 +02:00
ba95687f46 Merge branch 'signature-app/signature-doctrine-model' into 'signature-app-master'
Create entity workflow signature

See merge request Chill-Projet/chill-bundles!705
2024-07-02 06:35:23 +00:00
a309cc0774 Refactor workflow classes and forms
- the workflow controller add a context to each transition;
- the state of the entity workflow is applyied using a dedicated marking store
- the method EntityWorkflow::step use the context to associate the new step with the future destination user, cc users and email. This makes the step consistent at every step.
- this allow to remove some logic which was processed in eventSubscribers,
- as counterpart, each workflow must specify a dedicated marking_store:

```yaml
framework:
    workflows:
        vendee_internal:
            # ...
            marking_store:
                service: Chill\MainBundle\Workflow\EntityWorkflowMarkingStore
```
2024-07-02 08:30:28 +02:00
3db4fff80d Add signature functionality to workflow entities
Created new files to add signature functionality to the workflow entities, including signature state enums and signature metadata. Added these changes to the migration script as well. Updated EntityWorkflowStep to include a collection for signatures.
2024-07-01 14:50:03 +02:00
c9d54a5fea fix cs 2024-06-28 10:47:12 +02:00
86c862e69d Merge remote-tracking branch 'origin/upgrade-sf5' into signature-app-master 2024-06-28 10:41:52 +02:00
9bc6fe6aff Add PdfSignedMessage and its serializer
Added a new class, PdfSignedMessage, to handle received signed PDF messages. Also, added a serializer for this class, PdfSignedMessageSerializer, for use with messaging. Furthermore, comment documentation has been added to RequestPdfSignMessage and its serializer for better clarity. Updated unit tests are also included.
2024-06-27 21:52:24 +02:00
18a03fd740 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-27 13:38:19 +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
302 changed files with 2454 additions and 14681 deletions

View File

@@ -1,5 +0,0 @@
kind: Feature
body: Add job bundle (module emploi)
time: 2024-05-22T16:49:33.730465146+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,5 @@
kind: Feature
body: Metadata form added for person signatures
time: 2024-07-18T15:12:33.8134266+02:00
custom:
Issue: "288"

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

@@ -6,23 +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.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 ## v2.21.0 - 2024-06-18
### Feature ### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period

View File

@@ -31,6 +31,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",
@@ -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).
@@ -110,14 +110,15 @@ 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 - set up the jwt authentication bundle
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command
:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY`
and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill.
Prepare migrations and other tools Prepare migrations and other tools
********************************** **********************************
@@ -135,8 +136,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::

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",
@@ -58,7 +58,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

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

@@ -87,6 +87,7 @@
<li> <li>
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
hello
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span> </span>
{% else %} {% else %}

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

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

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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',
'Mme Caroline Diallo',
$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

@@ -0,0 +1,23 @@
<?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;
/**
* Message which is received when a pdf is signed.
*/
final readonly class PdfSignedMessage
{
public function __construct(
public readonly int $signatureId,
public readonly string $content
) {}
}

View File

@@ -0,0 +1,32 @@
<?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 Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
{
/**
* log prefix.
*/
private const P = '[pdf signed message] ';
public function __construct(
private LoggerInterface $logger,
) {}
public function __invoke(PdfSignedMessage $message): void
{
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
/**
* Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
*/
final readonly class PdfSignedMessageSerializer implements SerializerInterface
{
public function decode(array $encodedEnvelope): Envelope
{
$body = $encodedEnvelope['body'];
try {
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
}
if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
}
$content = base64_decode($decoded['content'], true);
if (false === $content) {
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
}
$message = new PdfSignedMessage($decoded['signatureId'], $content);
return new Envelope($message);
}
public function encode(Envelope $envelope): array
{
$message = $envelope->getMessage();
if (!$message instanceof PdfSignedMessage) {
throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
}
$data = [
'signatureId' => $message->signatureId,
'content' => base64_encode($message->content),
];
return [
'body' => json_encode($data, JSON_THROW_ON_ERROR),
'headers' => [],
];
}
}

View File

@@ -0,0 +1,29 @@
<?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;
/**
* Message which is sent when we request a signature on a pdf.
*/
final readonly class RequestPdfSignMessage
{
public function __construct(
public int $signatureId,
public PDFSignatureZone $PDFSignatureZone,
public int $signatureZoneIndex,
public string $reason,
public string $signerText,
public string $content,
) {}
}

View File

@@ -0,0 +1,105 @@
<?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;
/**
* Serialize a RequestPdfSignMessage, for external consumer.
*/
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'],
]);
$content = base64_decode($data['content'], true);
if (false === $content) {
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
}
$message = new RequestPdfSignMessage(
$data['signatureId'],
$zoneSignature,
$data['signatureZoneIndex'],
$data['reason'],
$data['signerText'],
$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,
'signerText' => $message->signerText,
'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

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
/**
* @internal
*
* @coversNothing
*/
class PdfSignedMessageSerializerTest extends TestCase
{
public function testDecode(): void
{
$asString = <<<'JSON'
{"signatureId": 0, "content": "dGVzdAo="}
JSON;
$actual = $this->buildSerializer()->decode(['body' => $asString]);
self::assertInstanceOf(Envelope::class, $actual);
$message = $actual->getMessage();
self::assertInstanceOf(PdfSignedMessage::class, $message);
self::assertEquals("test\n", $message->content);
self::assertEquals(0, $message->signatureId);
}
public function testEncode(): void
{
$envelope = new Envelope(
new PdfSignedMessage(0, "test\n")
);
$actual = $this->buildSerializer()->encode($envelope);
self::assertIsArray($actual);
self::assertArrayHasKey('body', $actual);
self::assertArrayHasKey('headers', $actual);
self::assertEquals([], $actual['headers']);
self::assertEquals(<<<'JSON'
{"signatureId":0,"content":"dGVzdAo="}
JSON, $actual['body']);
}
private function buildSerializer(): PdfSignedMessageSerializer
{
return new PdfSignedMessageSerializer();
}
}

View File

@@ -0,0 +1,137 @@
<?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',
'Mme Caroline Diallo',
'abc'
),
);
$actual = $serializer->encode($envelope);
$expectedBody = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'signerText' => $request->signerText,
'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',
'Mme Caroline Diallo',
'abc'
);
$bodyAsString = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'signerText' => $request->signerText,
'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->signerText, $actual->getMessage()->signerText);
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

@@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
{ {
return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
} }
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return false;
}
} }

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
);
} }
} }

View File

@@ -1,43 +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\Tests\Export;
use Chill\EventBundle\Export\Export\CountEventParticipations;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventParticipationsTest extends KernelTestCase
{
private CountEventParticipations $countEventParticipations;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->countEventParticipations = self::getContainer()->get(CountEventParticipations::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEventParticipations->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

View File

@@ -1,43 +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\Tests\Export;
use Chill\EventBundle\Export\Export\CountEvents;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventTest extends KernelTestCase
{
private CountEvents $countEvents;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->countEvents = self::getContainer()->get(CountEvents::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEvents->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

View File

@@ -1,59 +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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventDateAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::getContainer()->get(EventDateAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array|\Generator
{
yield ['frequency' => 'YYYY'];
yield ['frequency' => 'YYYY-MM'];
yield ['frequency' => 'YYYY-IV'];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

View File

@@ -1,59 +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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventTypeAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::getContainer()->get(EventTypeAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

View File

@@ -1,63 +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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Export\Aggregator\RoleAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::getContainer()->get(RoleAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Filter\EventDateFilter;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateFilterTest extends AbstractFilterTest
{
private RollingDateConverterInterface $rollingDateConverter;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->rollingDateConverter = self::getContainer()->get(RollingDateConverterInterface::class);
}
public function getFilter()
{
return new EventDateFilter($this->rollingDateConverter);
}
public function getFormData()
{
return [
[
'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'date_to' => new RollingDate(RollingDate::T_TODAY),
],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

View File

@@ -1,76 +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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Filter\EventTypeFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeFilterTest extends AbstractFilterTest
{
private EventTypeFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->filter = self::getContainer()->get(EventTypeFilter::class);
}
public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface
{
return $this->filter;
}
public function getFormData()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(EventType::class, 'et')
->select('et')
->getQuery()
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'types' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Filter\RoleFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleFilterTest extends AbstractFilterTest
{
private RoleFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->filter = self::getContainer()->get(RoleFilter::class);
}
public function getFilter()
{
return $this->filter;
}
public function getFormData(): array
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(Role::class, 'r')
->select('r')
->getQuery()
->setMaxResults(1)
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'roles' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Tests\Repository; namespace Chill\EventBundle\Tests\Repository;
use Chill\EventBundle\Repository\EventACLAwareRepository; use Chill\EventBundle\Repository\EventACLAwareRepository;
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\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;

View File

@@ -0,0 +1,18 @@
services:
chill_event.event_voter:
class: Chill\EventBundle\Security\Authorization\EventVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: security.voter }
chill_event.event_participation:
class: Chill\EventBundle\Security\Authorization\ParticipationVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: security.voter }

View File

@@ -1,41 +0,0 @@
services:
_defaults:
autowire: true
autoconfigure: true
# indicators
Chill\EventBundle\Export\Export\CountEvents:
tags:
- { name: chill.export, alias: 'count_events' }
Chill\EventBundle\Export\Export\CountEventParticipations:
tags:
- { name: chill.export, alias: 'count_event_participants' }
# filters
Chill\EventBundle\Export\Filter\EventDateFilter:
tags:
- { name: chill.export_filter, alias: 'event_date_filter' }
Chill\EventBundle\Export\Filter\EventTypeFilter:
tags:
- { name: chill.export_filter, alias: 'event_type_filter' }
Chill\EventBundle\Export\Filter\RoleFilter:
tags:
- { name: chill.export_filter, alias: 'role_filter' }
# aggregators
Chill\EventBundle\Export\Aggregator\EventTypeAggregator:
tags:
- { name: chill.export_aggregator, alias: event_type_aggregator }
Chill\EventBundle\Export\Aggregator\EventDateAggregator:
tags:
- { name: chill.export_aggregator, alias: event_date_aggregator }
Chill\EventBundle\Export\Aggregator\RoleAggregator:
tags:
- { name: chill.export_aggregator, alias: role_aggregator }

View File

@@ -1,14 +0,0 @@
services:
Chill\EventBundle\Security\EventVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }
Chill\EventBundle\Security\ParticipationVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@@ -19,9 +19,3 @@ events:
one {et un autre participant} one {et un autre participant}
other {et # autres participants} other {et # autres participants}
} }
ignored_participations: >-
{ count, plural,
one {La personne suivante a été ignorée parce qu''elle participe déjà à l''événement}
other {Les personnes suivantes ont été ignorées parce qu''elles participent déjà à l'événement}
}

View File

@@ -41,6 +41,7 @@ Back to the event: Retour à l'événement
The participation was created: La participation a été créée The participation was created: La participation a été créée
The participation was updated: La participation a été mise à jour The participation was updated: La participation a été mise à jour
'None of the requested people may participate the event: they are maybe already participating.': 'Aucune des personnes indiquées ne peut être ajoutée à l''événement: elles sont peut-être déjà inscrites comme participantes.' 'None of the requested people may participate the event: they are maybe already participating.': 'Aucune des personnes indiquées ne peut être ajoutée à l''événement: elles sont peut-être déjà inscrites comme participantes.'
'The following people have been ignored because they are already participating on the event': '{1} La personne suivante a été ignorée parce qu''elle participe déjà à l''événement | ]1,Inf] Les personnes suivantes ont été ignorées parce qu''elles participent déjà à l''événement'
There are no participation to edit for this event: Il n'y a pas de participation pour cet événement There are no participation to edit for this event: Il n'y a pas de participation pour cet événement
The participations have been successfully updated.: Les participations ont été mises à jour. The participations have been successfully updated.: Les participations ont été mises à jour.
The participation has been sucessfully removed: La participation a été correctement supprimée. The participation has been sucessfully removed: La participation a été correctement supprimée.
@@ -80,31 +81,9 @@ Pick an event: Choisir un événement
Pick a type of event: Choisir un type d'événement Pick a type of event: Choisir un type d'événement
Pick a moderator: Choisir un animateur Pick a moderator: Choisir un animateur
# exports
Select a format: Choisir un format Select a format: Choisir un format
Export: Exporter Export: Exporter
Count events: Nombre d'événements
Count events by various parameters.: Compte le nombre d'événements selon divers critères
Exports of events: Exports d'événements
Filtered by event date: Filtrer par date d'événement
'Filtered by date of event: only between %date_from% and %date_to%': "Filtré par date d'événement: uniquement entre le %date_from% et le %date_to%"
Events after this date: Événements après cette date
Events before this date: Événements avant cette date
Filtered by event type: Filtrer par type d'événement
'Filtered by event type: only %list%': "Filtré par type: uniquement %list%"
Group event by date: Grouper par date d'événement
Group by event type: Grouper par type d'événement
Count event participants: Nombre de participations
Count participants to an event by various parameters.: Compte le nombre de participations selon divers critères
Exports of event participants: Exports de participations
'Filtered by participant roles: only %list%': "Filtré par rôles de participation: uniquement %list%"
Filter by participant roles: Filtrer par rôles de participation
Part roles: Rôles de participation
Group by participant role: Grouper par rôle de participation
Events configuration: Configuration des événements Events configuration: Configuration des événements
Events configuration menu: Menu des événements Events configuration menu: Menu des événements

View File

@@ -1,84 +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\FranceTravailApiBundle\ApiHelper;
use Chill\MainBundle\Redis\ChillRedis;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
/**
* Wraps the pole emploi api.
*/
class ApiWrapper
{
/**
* @var Client
*/
private $client;
/**
* key for the bearer for the api pole emploi.
*
* This bearer is shared across users
*/
public const UNPERSONAL_BEARER = 'api_pemploi_bear_';
public function __construct(private $clientId, private $clientSecret, private readonly ChillRedis $redis)
{
$this->client = new Client([
'base_uri' => 'https://entreprise.francetravail.fr/connexion/oauth2/access_token',
]);
}
public function getPublicBearer($scopes): string
{
$cacheKey = $this->getCacheKey($scopes);
if ($this->redis->exists($cacheKey) > 0) {
$data = \unserialize($this->redis->get($cacheKey));
return $data->access_token;
}
try {
$response = $this->client->post('', [
'query' => ['realm' => '/partenaire'],
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'scope' => \implode(' ', \array_merge($scopes, ['application_'.$this->clientId])),
],
]);
} catch (ClientException $e) {
dump($e->getResponse());
}
$data = \json_decode((string) $response->getBody());
// set the key with an expiry time
$this->redis->setex(
$cacheKey,
$data->expires_in - 2,
\serialize($data)
);
return $data->access_token;
}
protected function getCacheKey($scopes)
{
return self::UNPERSONAL_BEARER.implode('', $scopes);
}
}

View File

@@ -1,113 +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\FranceTravailApiBundle\ApiHelper;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
/**
* Queries for ROME partenaires api.
*/
class PartenaireRomeAppellation
{
use ProcessRequestTrait;
/**
* @var ApiWrapper
*/
protected $wrapper;
/**
* @var Client
*/
protected $client;
/**
* @var LoggerInterface
*/
protected $logger;
private const BASE = 'https://api.pole-emploi.io/partenaire/rome-metiers/v1/metiers/';
public function __construct(
ApiWrapper $wrapper,
LoggerInterface $logger,
private \Symfony\Contracts\HttpClient\HttpClientInterface $httpClient,
) {
$this->wrapper = $wrapper;
$this->logger = $logger;
$this->client = new Client([
'base_uri' => 'https://api.pole-emploi.io/partenaire/rome-metiers/v1/metiers/',
]);
}
private function getBearer()
{
return $this->wrapper->getPublicBearer([
'api_rome-metiersv1',
'nomenclatureRome',
]);
}
public function getListeAppellation(string $search): array
{
$bearer = $this->getBearer();
try {
$response = $this->httpClient->request(
'GET',
self::BASE.'appellation/requete',
[
'headers' => [
'Authorization' => 'Bearer '.$bearer,
'Accept' => 'application/json',
],
'query' => [
'q' => $search,
],
]
);
return $response->toArray()['resultats'];
} catch (HttpExceptionInterface $exception) {
throw $exception;
}
}
public function getAppellation(string $code): array
{
$bearer = $this->getBearer();
while (true) {
try {
$response = $this->httpClient->request('GET', sprintf(self::BASE.'appellation/%s', $code), [
'headers' => [
'Authorization' => 'Bearer '.$bearer,
'Accept' => 'application/json',
],
'query' => [
'champs' => 'code,libelle,metier(code,libelle)',
],
]);
return $response->toArray();
} catch (HttpExceptionInterface $exception) {
if (429 === $exception->getResponse()->getStatusCode()) {
$retryAfter = $exception->getResponse()->getHeaders(false)['retry-after'][0] ?? 1;
sleep((int) $retryAfter);
} else {
throw $exception;
}
}
}
}
}

View File

@@ -1,100 +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\FranceTravailApiBundle\ApiHelper;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Psr7;
use Psr\Log\LoggerInterface;
/**
* Methods to process request against the api, and handle the
* Exceptions.
*/
trait ProcessRequestTrait
{
/**
* Handle a request and 429 errors.
*
* @param Request $request the request
* @param array $parameters the requests parameters
*/
protected function handleRequest(
Request $request,
array $parameters,
Client $client,
LoggerInterface $logger
) {
return $this->handleRequestRecursive(
$request,
$parameters,
$client,
$logger
);
}
/**
* internal method to handle recursive requests.
*
* @throws BadResponseException
*/
private function handleRequestRecursive(
Request $request,
array $parameters,
Client $client,
LoggerInterface $logger,
$counter = 0
) {
try {
return $client->send($request, $parameters);
} catch (BadResponseException $e) {
if (
// get 429 exceptions
$e instanceof ClientException
&& 429 === $e->getResponse()->getStatusCode()
&& count($e->getResponse()->getHeader('Retry-After')) > 0) {
if ($counter > 5) {
$logger->error('too much 429 response', [
'request' => Psr7\get($e->getRequest()),
]);
throw $e;
}
$delays = $e->getResponse()->getHeader('Retry-After');
$delay = \end($delays);
sleep($delay);
return $this->handleRequestRecursive(
$request,
$parameters,
$client,
$logger,
$counter + 1
);
}
// handling other errors
$logger->error('Error while querying ROME api', [
'status_code' => $e->getResponse()->getStatusCode(),
'part' => 'appellation',
'request' => $e->getRequest()->getBody()->getContents(),
'response' => $e->getResponse()->getBody()->getContents(),
]);
throw $e;
}
}
}

View File

@@ -1,16 +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\FranceTravailApiBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillFranceTravailApiBundle extends Bundle {}

View File

@@ -1,56 +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\FranceTravailApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Chill\FranceTravailApiBundle\ApiHelper\PartenaireRomeAppellation;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class RomeController extends AbstractController
{
/**
* @var PartenaireRomeAppellation
*/
protected $apiAppellation;
public function __construct(PartenaireRomeAppellation $apiAppellation)
{
$this->apiAppellation = $apiAppellation;
}
#[Route(path: '/{_locale}/france-travail/appellation/search.{_format}', name: 'chill_france_travail_api_appellation_search')]
public function appellationSearchAction(Request $request)
{
if (false === $request->query->has('q')) {
return new JsonResponse([]);
}
$appellations = $this->apiAppellation
->getListeAppellation($request->query->get('q'));
$results = [];
foreach ($appellations as $appellation) {
$appellation['id'] = 'original-'.$appellation['code'];
$appellation['text'] = $appellation['libelle'];
$results[] = $appellation;
}
$computed = new \stdClass();
$computed->pagination = (new \stdClass());
$computed->pagination->more = false;
$computed->results = $results;
return new JsonResponse($computed);
}
}

View File

@@ -1,52 +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\FranceTravailApiBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration.
*
* @see http://symfony.com/doc/current/cookbook/bundles/extension.html
*/
class ChillFranceTravailApiExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
public function prepend(ContainerBuilder $container): void
{
$this->prependRoute($container);
}
protected function prependRoute(ContainerBuilder $container): void
{
// declare routes for france travail api bundle
$container->prependExtensionConfig('chill_main', [
'routing' => [
'resources' => [
'@ChillFranceTravailApiBundle/Resources/config/routing.yml',
],
],
]);
}
}

View File

@@ -1,34 +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\FranceTravailApiBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
*/
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('chill_france_travail_api');
$rootNode = $treeBuilder->getRootNode();
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}

View File

@@ -1,3 +0,0 @@
chill_france_travail_api_controllers:
resource: "@ChillFranceTravailApiBundle/Controller"
type: annotation

View File

@@ -1,16 +0,0 @@
services:
_defaults:
autowire: true
autoconfigure: true
Chill\FranceTravailApiBundle\ApiHelper\ApiWrapper:
$clientId: '%env(FRANCE_TRAVAIL_CLIENT_ID)%'
$clientSecret: '%env(FRANCE_TRAVAIL_CLIENT_SECRET)%'
$redis: '@Chill\MainBundle\Redis\ChillRedis'
Chill\FranceTravailApiBundle\ApiHelper\PartenaireRomeAppellation: ~
Chill\FranceTravailApiBundle\Controller\RomeController:
autowire: true
autoconfigure: true
tags: ['controller.service_arguments']

View File

@@ -1,93 +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\FranceTravailApiBundle\Tests\ApiHelper;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Chill\FranceTravailApiBundle\ApiHelper\PartenaireRomeAppellation;
/**
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*
* @internal
*
* @coversNothing
*/
class PartenaireRomeAppellationTest extends KernelTestCase
{
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
}
public function testGetListeMetiersSimple()
{
/** @var PartenaireRomeAppellation $appellations */
$appellations = self::$kernel
->getContainer()
->get(PartenaireRomeAppellation::class)
;
$data = $appellations->getListeAppellation('arb');
$this->assertTrue(\is_array($data));
$this->assertNotNull($data[0]->libelle);
$this->assertNotNull($data[0]->code);
}
public function testGetListeMetiersTooMuchRequests()
{
/** @var PartenaireRomeMetier $appellations */
$appellations = self::$kernel
->getContainer()
->get(PartenaireRomeAppellation::class)
;
$appellations->getListeAppellation('arb');
$appellations->getListeAppellation('ing');
$appellations->getListeAppellation('rob');
$appellations->getListeAppellation('chori');
$data = $appellations->getListeAppellation('camion');
$this->assertTrue(
$data[0] instanceof \stdClass,
'assert that first index of data is an instance of stdClass'
);
}
public function testGetAppellation()
{
/** @var PartenaireRomeMetier $appellations */
$appellations = self::$kernel
->getContainer()
->get(PartenaireRomeAppellation::class)
;
$a = $appellations->getListeAppellation('arb');
$full = $appellations->getAppellation($a[0]->code);
$this->assertNotNull(
$full->libelle,
'assert that libelle is not null'
);
$this->assertTrue(
$full->metier instanceof \stdClass,
'assert that metier is returned'
);
$this->assertNotNull(
$full->metier->libelle,
'assert that metier->libelle is not null'
);
}
}

View File

@@ -1,29 +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\FranceTravailApiBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
*
* @coversNothing
*/
class RomeControllerTest extends WebTestCase
{
public function testAppellationsearch()
{
$client = static::createClient();
$crawler = $client->request('GET', '/{_locale}/pole-emploi/appellation/search');
}
}

View File

@@ -1,16 +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\JobBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillJobBundle extends Bundle {}

View File

@@ -1,123 +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\JobBundle\Controller;
use Chill\PersonBundle\CRUD\Controller\EntityPersonCRUDController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Chill\JobBundle\Entity\Immersion;
use Symfony\Component\HttpFoundation\Response;
/**
* CRUD Controller for reports (Frein, ...).
*/
class CSCrudReportController extends EntityPersonCRUDController
{
protected function onBeforeRedirectAfterSubmission(string $action, $entity, FormInterface $form, Request $request): ?Response
{
$next = $request->request->get('submit', 'save-and-close');
return match ($next) {
'save-and-close', 'delete-and-close' => $this->redirectToRoute('chill_job_report_index', [
'person' => $entity->getPerson()->getId(),
]),
default => parent::onBeforeRedirectAfterSubmission($action, $entity, $form, $request),
};
}
protected function duplicateEntity(string $action, Request $request)
{
if ('cscv' === $this->getCrudName()) {
$id = $request->query->get('duplicate_id', 0);
/** @var \Chill\JobBundle\Entity\CV $cv */
$cv = $this->getEntity($action, $id, $request);
$em = $this->managerRegistry->getManager();
$em->detach($cv);
foreach ($cv->getExperiences() as $experience) {
$cv->removeExperience($experience);
$em->detach($experience);
$cv->addExperience($experience);
}
foreach ($cv->getFormations() as $formation) {
$cv->removeFormation($formation);
$em->detach($formation);
$cv->addFormation($formation);
}
return $cv;
}
if ('projet_prof' === $this->getCrudName()) {
$id = $request->query->get('duplicate_id', 0);
/** @var \Chill\JobBundle\Entity\ProjetProfessionnel $original */
$original = $this->getEntity($action, $id, $request);
$new = parent::duplicateEntity($action, $request);
foreach ($original->getSouhait() as $s) {
$new->addSouhait($s);
}
foreach ($original->getValide() as $s) {
$new->addValide($s);
}
return $new;
}
return parent::duplicateEntity($action, $request);
}
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
{
if ($entity instanceof Immersion) {
if ('edit' === $action || 'new' === $action) {
return parent::createFormFor($action, $entity, $formClass, [
'center' => $entity->getPerson()->getCenter(),
]);
}
if ('bilan' === $action) {
return parent::createFormFor($action, $entity, $formClass, [
'center' => $entity->getPerson()->getCenter(),
'step' => 'bilan',
]);
}
if ('delete' === $action) {
return parent::createFormFor($action, $entity, $formClass, $formOptions);
}
throw new \LogicException("this step {$action} is not supported");
}
return parent::createFormFor($action, $entity, $formClass, $formOptions);
}
protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request)
{
// for immersion / edit-bilan action
if ('bilan' === $action) {
/* @var $entity Immersion */
$entity->setIsBilanFullfilled(true);
}
parent::onPreFlush($action, $entity, $form, $request);
}
/**
* Edit immersion bilan.
*
* @param int $id
*/
public function editBilan(Request $request, $id): Response
{
return $this->formEditAction('bilan', $request, $id);
}
}

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