Compare commits

..

35 Commits

Author SHA1 Message Date
3b82ab0e7f Release v4.0.0 2025-07-08 15:59:04 +02:00
ccfae1dc75 Merge branch '339-partage-d'export-enregistré' into 'master'
Partage d'export enregistré et génération asynchrone des exports

Closes #339 and #338

See merge request Chill-Projet/chill-bundles!800
2025-07-08 13:53:38 +00:00
8bc16dadb0 Partage d'export enregistré et génération asynchrone des exports 2025-07-08 13:53:25 +00:00
c4cc0baa8e Merge branch 'eslint-fix-issues-2025-07' into 'master'
Fix Eslint issues

See merge request Chill-Projet/chill-bundles!853
2025-07-08 13:38:51 +00:00
aed114c75c Fix Eslint issues 2025-07-08 13:38:51 +00:00
e592b89c94 remove ux-translator from dependencies 2025-07-07 12:36:49 +02:00
70e75adb7d Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-07-02 13:57:20 +02:00
6f7015b152 Fix translations of form fields in admin for social actions 2025-07-02 13:56:41 +02:00
65dde1e6a0 Merge branch '390-fix-search-results' into 'master'
Fix participant condition in list_with_period.html.twig

Closes #390

See merge request Chill-Projet/chill-bundles!832
2025-07-02 10:59:17 +00:00
d193c50922 Merge branch '359-fusion-accompanying-period-work' into 'master'
Resolve "Fusion actions d'accompagnement"

Closes #359

See merge request Chill-Projet/chill-bundles!804
2025-07-02 10:53:17 +00:00
840ef6eed8 Resolve "Fusion actions d'accompagnement" 2025-07-02 10:53:16 +00:00
b4bbb1a456 Merge branch 'improve_person_resource_form' into 'master'
Improve person resource form

See merge request Chill-Projet/chill-bundles!846
2025-07-01 14:01:44 +00:00
606435a6b3 Pipeline corrections 2025-07-01 14:47:36 +02:00
404143f8a6 Merge branch 'improve_person_resource_form' of https://gitlab.com/Chill-Projet/chill-bundles into improve_person_resource_form 2025-07-01 14:34:39 +02:00
ec957a2fe3 Improve UX with better labeling in person resource form 2025-07-01 14:34:19 +02:00
8ed5e35f1a Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-07-01 14:33:27 +02:00
ec37676dab release v3.12.1 2025-06-30 20:35:00 +02:00
2d8cda30b9 Add localizeString method to PickTemplate component for string localization 2025-06-30 20:32:17 +02:00
27d344c97d Release v3.12.0 2025-06-30 11:00:42 +02:00
088e5692e2 Merge branch 'improve_person_resource_form' into 'master'
Improve admin templates for event admin entities + activity reason (category)...

See merge request Chill-Projet/chill-bundles!838
2025-06-30 08:44:24 +00:00
298044bc82 Improve admin templates for event admin entities + activity reason (category)... 2025-06-30 08:44:24 +00:00
ee4e223043 Merge branch '393-fix-dump-only-document-generator' into 'master'
Send data dumps as email attachments instead of links, update translations,...

Closes #393

See merge request Chill-Projet/chill-bundles!843
2025-06-30 08:41:09 +00:00
c53377ce8d Merge branch 'workflow-do-not-remove-workflow-canceled-automatically' into 'master'
Remove unnecessary workflow deletion logic when in the initial position

See merge request Chill-Projet/chill-bundles!844
2025-06-30 08:40:46 +00:00
0b580658de Remove unnecessary workflow deletion logic when in the initial position 2025-06-26 14:38:12 +02:00
786c60a50d Send data dumps as email attachments instead of links, update translations, and add unit tests for the handler. 2025-06-26 12:21:19 +02:00
456f00566d update juni guidelines 2025-06-26 12:19:49 +02:00
a38116cca4 fix cs 2025-06-20 17:31:13 +02:00
nobohan
9158e33854 #392 php cs-fixer 2025-06-19 21:29:43 +02:00
nobohan
af74f7860b Fixed nullable content for NewsItem in setContent signature #392 2025-06-19 17:53:33 +02:00
c2842148c6 Improve UX with better labeling in person resource form 2025-06-17 19:27:06 +02:00
10e4c7da23 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-06-17 18:45:25 +02:00
f680a35f49 Improve admin templates for event admin entities + activity reason (category) entities and remove delete and show actions for coherence 2025-06-11 17:13:19 +02:00
7d0fe06651 Fix admin entity edit actions for event admin entities and activity reason (category) entities 2025-06-11 16:46:04 +02:00
fca10ada71 Fix translation keys and participant pluralization in list_with_period.html.twig
Updated French translations for "Participants" and improved pluralization handling in accompanying_period keys. Modified list_with_period.html.twig to dynamically translate "Participants" based on the count of current participations.
2025-06-04 16:55:56 +02:00
a35d456308 Fix participant condition in list_with_period.html.twig
Updated the condition to properly handle cases where the first participation's person is not the current person.
2025-06-04 16:54:44 +02:00
178 changed files with 3698 additions and 4674 deletions

View File

@@ -1,6 +0,0 @@
kind: DX
body: Allow TranslatableMessage in flash messages
time: 2025-04-01T14:47:28.814268801+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,44 +0,0 @@
kind: DX
body: |
Rewrite exports to run them asynchronously
changelog: |
- Add new methods to serialize data using the rector rule
- Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`.
- Refactor manually the initializeQuery method
- Remove the injection of ExportManager into the constructor of each export element:
```diff
- class MyFormatter implements FormatterInterface
+ class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface
{
+ use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
- public function __construct(private ExportManager $exportmanager) {}
public function MyMethod(): void
{
- $this->exportManager->getFilter('alias');
+ $this->getExportManager()->getFilter('alias');
}
}
```
- configure messenger to handle export in a queue:
```diff
# config/packages/messenger.yaml
framework:
messenger:
routing:
+ 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
```
time: 2025-04-07T12:10:10.682561327+02:00
custom:
Issue: ""
SchemaChange: Add columns or tables

View File

@@ -1,6 +0,0 @@
kind: DX
body: Remove dead code for wopi-link module
time: 2025-04-30T14:45:50.406111606+02:00
custom:
Issue: "352"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: DX
body: Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
time: 2025-05-28T16:58:13.226870341+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Feature
body: Add the document file name to the document title when a user upload a document,
unless there is already a document title.
time: 2025-04-24T14:22:11.800975422+02:00
custom:
Issue: "377"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add desactivation date for social action and issue csv export
time: 2025-05-20T09:56:28.108941934+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add Emoji and Fullscreen feature to ckeditor configuration
time: 2025-05-23T13:33:41.645095128+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Create editor which allow us to toggle between rich and simple text editor
time: 2025-05-23T13:34:34.56795603+02:00
custom:
Issue: "321"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Fixed
body: trying to prevent bug of typeerror in doc-history + improved display of document
history
time: 2025-04-24T13:39:43.878468232+02:00
custom:
Issue: "376"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Fixed
body: Display previous participation in acc course work even if the person has left
the acc course
time: 2025-04-24T16:37:46.970203594+02:00
custom:
Issue: "381"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix display of text in calendar events
time: 2025-05-05T10:27:15.461493066+02:00
custom:
Issue: "372"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Add missing translation for user_group.no_user_groups
time: 2025-05-14T14:53:39.53927329+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix retrieve schema to form full tablename and construct sql statements correctly in Thirdparty merger.
time: 2025-05-20T14:00:08.987229634+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix add missing translation
time: 2025-05-20T14:04:33.612140549+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix the transfer of evaluations and documents during of accompanyingperiodwork
time: 2025-05-20T16:44:29.093304653+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
time: 2025-04-23T17:26:24.45777387+02:00
custom:
Issue: "374"
SchemaChange: No schema change

22
.changes/v3.12.0.md Normal file
View File

@@ -0,0 +1,22 @@
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form

3
.changes/v3.12.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.12.1 - 2025-06-30
### Fixed
* Fix loading of the list of documents

74
.changes/v4.0.0.md Normal file
View File

@@ -0,0 +1,74 @@
## v4.0.0 - 2025-07-08
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
### Fixed
* ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* Fix translations for social action fields in admin form: results, goals, evaluations
### DX
* Rewrite exports to run them asynchronously
**Schema Change**: Add columns or tables
* Allow TranslatableMessage in flash messages
### UX
* Improve labeling of fields in person resource creation form
**Release notes**
- Add new methods to serialize data using the rector rule
- Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`.
- Refactor manually the initializeQuery method
- Remove the injection of ExportManager into the constructor of each export element:
```diff
- class MyFormatter implements FormatterInterface
+ class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface
{
+ use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
- public function __construct(private ExportManager $exportmanager) {}
public function MyMethod(): void
{
- $this->exportManager->getFilter('alias');
+ $this->getExportManager()->getFilter('alias');
}
}
```
- configure messenger to handle export in a queue:
```diff
# config/packages/messenger.yaml
framework:
messenger:
routing:
+ 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
```
- add missing methods to exports, aggregators, filters, formatter:
```php
public function normalizeFormData(array $formData): array;
public function denormalizeFormData(array $formData, int $fromVersion): array;
```
There are rector rules to generate those methods:
- `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector`
See:
```php
// upgrade chill exports
$rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]);
```
This rule will create most of the work necessary, but some manuals changes are still necessary:
- we must set manually the correct repository for method `denormalizeDoctrineEntity`;
- when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`)

View File

@@ -22,7 +22,7 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
- **Backend**: PHP 8.3+, Symfony 5.4
- **Frontend**: JavaScript/TypeScript, Vue.js 3, Bootstrap 5
- **Build Tools**: Webpack Encore, Yarn
- **Database**: PostgreSQL with materialized views
- **Database**: PostgreSQL with materialized views. We do not support other databases.
- **Other Services**: Redis, AMQP (RabbitMQ), SMTP
## Project Structure
@@ -149,6 +149,42 @@ Key configuration files:
- `package.json`: JavaScript dependencies and scripts
- `.env`: Default environment variables. Must usually not be updated: use `.env.local` instead.
### Database migrations
Each time a doctrine entity is created, we generate migration to adapt the database.
The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
- `Chill\Bundle\CustomFieldsBundle` writes migrations to `Chill\Migrations\CustomFields`;
- `Chill\Bundle\DocGeneratorBundle` writes migrations to `Chill\Migrations\DocGenerator`;
- `Chill\Bundle\DocStoreBundle` writes migrations to `Chill\Migrations\DocStore`;
- `Chill\Bundle\EventBundle` writes migrations to `Chill\Migrations\Event`;
- `Chill\Bundle\CalendarBundle` writes migrations to `Chill\Migrations\Calendar`;
- `Chill\Bundle\FamilyMembersBundle` writes migrations to `Chill\Migrations\FamilyMembers`;
- `Chill\Bundle\FranceTravailApiBundle` writes migrations to `Chill\Migrations\FranceTravailApi`;
- `Chill\Bundle\JobBundle` writes migrations to `Chill\Migrations\Job`;
- `Chill\Bundle\MainBundle` writes migrations to `Chill\Migrations\Main`;
- `Chill\Bundle\PersonBundle` writes migrations to `Chill\Migrations\Person`;
- `Chill\Bundle\ReportBundle` writes migrations to `Chill\Migrations\Report`;
- `Chill\Bundle\TaskBundle` writes migrations to `Chill\Migrations\Task`;
- `Chill\Bundle\ThirdPartyBundle` writes migrations to `Chill\Migrations\ThirdParty`;
- `Chill\Bundle\TicketBundle` writes migrations to `Chill\Migrations\Ticket`;
- `Chill\Bundle\WopiBundle` writes migrations to `Chill\Migrations\Wopi`;
Once created the, comment's classes should be removed and a description of the changes made to the entities should be added to the migrations, using the `getDescription` method. The migration should not be cleaned by any artificial intelligence, as modifying this migration is error prone.
### Guidelines related to code structure and requirements
#### Usage of clock
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
where injection does not work when restoring an entity from database, but usually possible in services.
### Testing Information
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
@@ -218,7 +254,7 @@ class TicketTest extends TestCase
#### Test Database
For tests that require a database, the project uses an in-memory SQLite database by default. You can configure a different database for testing in the `.env.test` file.
For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
### Code Quality Tools

View File

@@ -6,6 +6,128 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.0.0 - 2025-07-08
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
### Fixed
* ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* Fix translations for social action fields in admin form: results, goals, evaluations
### DX
* Rewrite exports to run them asynchronously
**Schema Change**: Add columns or tables
* Allow TranslatableMessage in flash messages
### UX
* Improve labeling of fields in person resource creation form
**Release notes**
- Add new methods to serialize data using the rector rule
- Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`.
- Refactor manually the initializeQuery method
- Remove the injection of ExportManager into the constructor of each export element:
```diff
- class MyFormatter implements FormatterInterface
+ class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface
{
+ use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
- public function __construct(private ExportManager $exportmanager) {}
public function MyMethod(): void
{
- $this->exportManager->getFilter('alias');
+ $this->getExportManager()->getFilter('alias');
}
}
```
- configure messenger to handle export in a queue:
```diff
# config/packages/messenger.yaml
framework:
messenger:
routing:
+ 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
```
- add missing methods to exports, aggregators, filters, formatter:
```php
public function normalizeFormData(array $formData): array;
public function denormalizeFormData(array $formData, int $fromVersion): array;
```
There are rector rules to generate those methods:
- `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector`
See:
```php
// upgrade chill exports
$rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]);
```
This rule will create most of the work necessary, but some manuals changes are still necessary:
- we must set manually the correct repository for method `denormalizeDoctrineEntity`;
- when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`)
## v3.12.1 - 2025-06-30
### Fixed
* Fix loading of the list of documents
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form
## v3.11.0 - 2025-04-17
### Feature
* ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities.
* ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number
**Schema Change**: Add columns or tables
* Signature: add a button to go directly to the signature zone, even if there is only one
### Fixed
* Fixed wrong translations in the on-the-fly for creation of thirdParty
* Fixed update of phone number in on-the-fly edition of thirdParty
* Fixed closing of modal when editing thirdParty in accompanying course works
* Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day
* ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list
* When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk
* Add consistent log prefix and key to logs when stale workflows are automatically canceled
* ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition
### DX
* Add new chill-col style for displaying title and aside in a flex table
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes

View File

@@ -11,6 +11,7 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@@ -2154,11 +2154,6 @@ parameters:
count: 1
path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php
-
message: "#^Instanceof between string and DateTimeInterface will always evaluate to false\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php
-
message: "#^PHPDoc tag @var for property Chill\\\\MainBundle\\\\Export\\\\Helper\\\\ExportAddressHelper\\:\\:\\$unitNamesKeysCache contains unresolvable type\\.$#"
count: 1

View File

@@ -37,9 +37,6 @@ return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);
// upgrade chill exports
$rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]);
// part of the symfony 54 rules
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
$rectorConfig->rule(\Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class);

View File

@@ -48,28 +48,6 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/edit', name: 'chill_activity_activityreasoncategory_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReasonCategory entities.
*/
@@ -100,29 +78,10 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/show', name: 'chill_activity_activityreasoncategory_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
return $this->render('@ChillActivity/ActivityReasonCategory/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@@ -139,7 +98,7 @@ class ActivityReasonCategoryController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em->flush();
return $this->redirectToRoute('chill_activity_activityreasoncategory_edit', ['id' => $id]);
return $this->redirectToRoute('chill_activity_activityreasoncategory', ['id' => $id]);
}
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
@@ -178,7 +137,7 @@ class ActivityReasonCategoryController extends AbstractController
{
$form = $this->createForm(ActivityReasonCategoryType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreasoncategory_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -17,7 +17,6 @@ use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* ActivityReason controller.
@@ -50,28 +49,6 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/edit', name: 'chill_activity_activityreason_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (null === $entity) {
throw new NotFoundHttpException('Unable to find ActivityReason entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReason/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReason entities.
*/
@@ -102,29 +79,10 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/show', name: 'chill_activity_activityreason_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReason entity.');
}
return $this->render('@ChillActivity/ActivityReason/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@@ -180,7 +138,7 @@ class ActivityReasonController extends AbstractController
{
$form = $this->createForm(ActivityReasonType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -2,7 +2,7 @@ import "es6-promise/auto";
import { createStore } from "vuex";
import { postLocation } from "./api";
import prepareLocations from "./store.locations.js";
import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods";
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity);
@@ -369,7 +369,7 @@ const store = createStore({
// console.log('works', works);
commit("setAccompanyingPeriodWorks", works);
} catch (error) {
console.error('Failed to fetch works:', error);
console.error("Failed to fetch works:", error);
}
},
getWhoAmI({ commit }) {

View File

@@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReason list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -29,10 +29,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreason_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReasonCategory list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -23,10 +23,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreasoncategory_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreasoncategory_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreasoncategory_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@@ -22,6 +22,52 @@ use Symfony\Component\Security\Core\Role\Role;
*/
final class ActivityControllerTest extends WebTestCase
{
/**
* @dataProvider getSecuredPagesUnauthenticated
*/
public function testAccessIsDeniedForUnauthenticated(mixed $url)
{
$client = $this->createClient();
$client->request('GET', $url);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertTrue(
$client->getResponse()->isRedirect('http://localhost/login'),
sprintf('the page "%s" does not redirect to http://localhost/login', $url)
);
}
/**
* Provide a client unauthenticated and.
*/
public function getSecuredPagesUnauthenticated()
{
self::bootKernel();
$person = $this->getPersonFromFixtures();
$activities = $this->getActivitiesForPerson($person);
return [
[sprintf('fr/person/%d/activity/', $person->getId())],
[sprintf('fr/person/%d/activity/new', $person->getId())],
[sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())],
[sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())],
];
}
/**
* @dataProvider getSecuredPagesAuthenticated
*
* @param type $client
* @param type $url
*/
public function testAccessIsDeniedForUnauthorized($client, $url)
{
$client->request('GET', $url);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function getSecuredPagesAuthenticated()
{
self::bootKernel();
@@ -55,52 +101,6 @@ final class ActivityControllerTest extends WebTestCase
];
}
/**
* Provide a client unauthenticated and.
*/
public function getSecuredPagesUnauthenticated()
{
self::bootKernel();
$person = $this->getPersonFromFixtures();
$activities = $this->getActivitiesForPerson($person);
return [
[sprintf('fr/person/%d/activity/', $person->getId())],
[sprintf('fr/person/%d/activity/new', $person->getId())],
[sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())],
[sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())],
];
}
/**
* @dataProvider getSecuredPagesUnauthenticated
*/
public function testAccessIsDeniedForUnauthenticated(mixed $url)
{
$client = $this->createClient();
$client->request('GET', $url);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertTrue(
$client->getResponse()->isRedirect('http://localhost/login'),
sprintf('the page "%s" does not redirect to http://localhost/login', $url)
);
}
/**
* @dataProvider getSecuredPagesAuthenticated
*
* @param type $client
* @param type $url
*/
public function testAccessIsDeniedForUnauthorized($client, $url)
{
$client->request('GET', $url);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function testCompleteScenario()
{
// Create a new client to browse the application

View File

@@ -137,6 +137,64 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
self::assertIsArray($actual);
}
public function provideDataFindByAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException('no period found');
}
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException('no types');
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
$job = new UserJob();
$job->setLabel(['fr' => 'test']);
$this->entityManager->persist($job);
$this->entityManager->flush();
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException('no user found');
}
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
/**
* @dataProvider provideDataFindByPerson
*/
@@ -291,62 +349,4 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
public function provideDataFindByAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException('no period found');
}
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException('no types');
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
$job = new UserJob();
$job->setLabel(['fr' => 'test']);
$this->entityManager->persist($job);
$this->entityManager->flush();
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException('no user found');
}
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
}

View File

@@ -57,6 +57,46 @@ final class ActivityVoterTest extends KernelTestCase
$this->prophet = new \Prophecy\Prophet();
}
public function testNullUser()
{
$token = $this->prepareToken();
$center = $this->prepareCenter(1, 'center');
$person = $this->preparePerson($center);
$scope = $this->prepareScope(1, 'default');
$activity = $this->prepareActivity($scope, $person);
$this->assertEquals(
VoterInterface::ACCESS_DENIED,
$this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']),
'assert that a null user is not allowed to see'
);
}
/**
* @dataProvider dataProvider_testVoteAction
*
* @param type $expectedResult
* @param string $attribute
* @param string $message
*/
public function testVoteAction(
$expectedResult,
User $user,
Scope $scope,
Center $center,
$attribute,
$message,
) {
$token = $this->prepareToken($user);
$activity = $this->prepareActivity($scope, $this->preparePerson($center));
$this->assertEquals(
$expectedResult,
$this->voter->vote($token, $activity, [$attribute]),
$message
);
}
public function dataProvider_testVoteAction()
{
$centerA = $this->prepareCenter(1, 'center A');
@@ -110,46 +150,6 @@ final class ActivityVoterTest extends KernelTestCase
];
}
public function testNullUser()
{
$token = $this->prepareToken();
$center = $this->prepareCenter(1, 'center');
$person = $this->preparePerson($center);
$scope = $this->prepareScope(1, 'default');
$activity = $this->prepareActivity($scope, $person);
$this->assertEquals(
VoterInterface::ACCESS_DENIED,
$this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']),
'assert that a null user is not allowed to see'
);
}
/**
* @dataProvider dataProvider_testVoteAction
*
* @param type $expectedResult
* @param string $attribute
* @param string $message
*/
public function testVoteAction(
$expectedResult,
User $user,
Scope $scope,
Center $center,
$attribute,
$message,
) {
$token = $this->prepareToken($user);
$activity = $this->prepareActivity($scope, $this->preparePerson($center));
$this->assertEquals(
$expectedResult,
$this->voter->vote($token, $activity, [$attribute]),
$message
);
}
/**
* prepare a token interface with correct rights.
*

View File

@@ -30,6 +30,18 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAsideActivityId
*/
public function testEditWithoutUsers(int $asideActivityId)
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit");
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
public static function generateAsideActivityId(): iterable
{
self::bootKernel();
@@ -58,18 +70,6 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAsideActivityId
*/
public function testEditWithoutUsers(int $asideActivityId)
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit");
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
public function testIndexWithoutUsers()
{
self::ensureKernelShutdown();

View File

@@ -42,6 +42,32 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testList(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testNew(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public static function provideAccompanyingPeriod(): iterable
{
self::bootKernel();
@@ -82,30 +108,4 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testList(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testNew(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
}

View File

@@ -45,20 +45,6 @@ class MSUserAbsenceReaderTest extends TestCase
self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message);
}
public function testIsUserAbsentWithoutRemoteId(): void
{
$user = new User();
$client = new MockHttpClient();
$mapUser = $this->prophesize(MapCalendarToUser::class);
$mapUser->getUserId($user)->willReturn(null);
$clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
$absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null');
}
public static function provideDataTestUserAbsence(): iterable
{
// contains data that was retrieved from microsoft graph api on 2023-07-06
@@ -173,4 +159,18 @@ class MSUserAbsenceReaderTest extends TestCase
'User is absent: absence is always enabled',
];
}
public function testIsUserAbsentWithoutRemoteId(): void
{
$user = new User();
$client = new MockHttpClient();
$mapUser = $this->prophesize(MapCalendarToUser::class);
$mapUser->getUserId($user)->willReturn(null);
$clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
$absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null');
}
}

View File

@@ -28,6 +28,24 @@ use PHPUnit\Framework\TestCase;
*/
final class DefaultRangeGeneratorTest extends TestCase
{
/**
* @dataProvider generateData
*/
public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate)
{
$generator = new DefaultRangeGenerator();
['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date);
if (null === $startDate) {
$this->assertNull($actualStartDate);
$this->assertNull($actualEndDate);
} else {
$this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM));
$this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM));
}
}
/**
* * Lundi => Envoi des rdv du mardi et mercredi.
* * Mardi => Envoi des rdv du jeudi.
@@ -79,22 +97,4 @@ final class DefaultRangeGeneratorTest extends TestCase
null,
];
}
/**
* @dataProvider generateData
*/
public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate)
{
$generator = new DefaultRangeGenerator();
['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date);
if (null === $startDate) {
$this->assertNull($actualStartDate);
$this->assertNull($actualEndDate);
} else {
$this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM));
$this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM));
}
}
}

View File

@@ -49,80 +49,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase
parent::tearDown();
}
/**
* provide empty data in different possible representations.
* Those data are supposed to be deserialized.
*
* @return array
*/
public static function emptyDataProvider()
{
return [
// 0
[
// signle
'',
],
// 1
[
// single
null,
],
// 2
[
// signle with allow other
['_other' => 'something', '_choices' => ''],
],
// 3
[
// multiple
[],
],
// 4
[
// multiple with allow other
['_other' => 'something', '_choices' => []],
],
// 5
[
// multiple with allow other
['_other' => '', '_choices' => []],
],
// 6
[
// empty
['_other' => null, '_choices' => null],
],
// 7
[
// empty
[null],
],
];
}
public static function serializedRepresentationDataProvider()
{
return [
[
// multiple => false, allow_other => false
'my-value',
],
[
// multiple => true, allow_ther => false
['my-value'],
],
[
// multiple => false, allow_other => true, current value not in other
['_other' => '', '_choices' => 'my-value'],
],
[
// multiple => true, allow_other => true, current value not in other
['_other' => '', '_choices' => ['my-value']],
],
];
}
/**
* Test if the representation of the data is deserialized to an array text
* with an "allow_other" field.
@@ -412,6 +338,58 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertTrue($isEmpty);
}
/**
* provide empty data in different possible representations.
* Those data are supposed to be deserialized.
*
* @return array
*/
public static function emptyDataProvider()
{
return [
// 0
[
// signle
'',
],
// 1
[
// single
null,
],
// 2
[
// signle with allow other
['_other' => 'something', '_choices' => ''],
],
// 3
[
// multiple
[],
],
// 4
[
// multiple with allow other
['_other' => 'something', '_choices' => []],
],
// 5
[
// multiple with allow other
['_other' => '', '_choices' => []],
],
// 6
[
// empty
['_other' => null, '_choices' => null],
],
// 7
[
// empty
[null],
],
];
}
// ///////////////////////////////////////
//
// test function isEmptyValue
@@ -435,6 +413,28 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertFalse($isEmpty);
}
public static function serializedRepresentationDataProvider()
{
return [
[
// multiple => false, allow_other => false
'my-value',
],
[
// multiple => true, allow_ther => false
['my-value'],
],
[
// multiple => false, allow_other => true, current value not in other
['_other' => '', '_choices' => 'my-value'],
],
[
// multiple => true, allow_other => true, current value not in other
['_other' => '', '_choices' => ['my-value']],
],
];
}
/**
* @param array $options
*

View File

@@ -58,6 +58,7 @@
<script>
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
export default {
name: "PickTemplate",
@@ -113,6 +114,9 @@ export default {
},
},
methods: {
localizeString(str) {
return localizeString(str);
},
clickGenerate(event, link) {
if (!this.preventDefaultMoveToGenerate) {
window.location.assign(link);

View File

@@ -1,7 +1,5 @@
{{ 'docgen.data_dump_email.Dear'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_attached'|trans }}
{{ link }}
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
{{ 'docgen.data_dump_email.filename'|trans({filename: filename}) }}

View File

@@ -11,13 +11,13 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -37,15 +37,15 @@ class RequestGenerationHandler implements MessageHandlerInterface
private const LOG_PREFIX = '[docgen message handler] ';
public function __construct(
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Generator $generator,
private readonly GeneratorInterface $generator,
private readonly LoggerInterface $logger,
private readonly StoredObjectRepository $storedObjectRepository,
private readonly StoredObjectRepositoryInterface $storedObjectRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly TranslatorInterface $translator,
private readonly StoredObjectManagerInterface $storedObjectManager,
) {}
public function __invoke(RequestGenerationMessage $message)
@@ -90,7 +90,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
$this->sendDataDump($destinationStoredObject, $message);
} else {
$destinationStoredObject = $this->generator->generateDocFromTemplate(
$this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
@@ -122,19 +122,20 @@ class RequestGenerationHandler implements MessageHandlerInterface
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{
$url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600);
$parts = [];
parse_str(parse_url($url->url)['query'], $parts);
$validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']);
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();
$contentType = $destinationStoredObject->getType();
// Create the email with the document as an attachment
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'link' => $url->url,
'validity' => $validity,
'filename' => $filename,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'));
->subject($this->translator->trans('docgen.data_dump_email.subject'))
->attach($content, $filename, $contentType);
$this->mailer->send($email);
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Tests\Service\Messenger;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationHandler;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class RequestGenerationHandlerTest extends TestCase
{
use ProphecyTrait;
public function testGenerationHappyScenario(): void
{
// Create entities
$template = new DocGeneratorTemplate();
$this->setPrivateProperty($template, 'id', 1);
$storedObject = new StoredObject();
$this->setPrivateProperty($storedObject, 'id', 2);
$creator = new User();
$creator->setEmail('test@example.com');
$this->setPrivateProperty($creator, 'id', 3);
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
$docGeneratorTemplateRepository->find(1)->willReturn($template);
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
$storedObjectRepository->find(2)->willReturn($storedObject);
$userRepository = $this->prophesize(UserRepositoryInterface::class);
$userRepository->find(3)->willReturn($creator);
// Create a mock for the Query object
$query = $this->prophesize(Query::class);
$query->setParameter('id', 2)->willReturn($query->reveal());
$query->execute()->shouldBeCalled();
// Create a mock for the EntityManager
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->createQuery(Argument::containingString('UPDATE'))->willReturn($query->reveal());
$entityManager->flush()->shouldBeCalled();
$generator = $this->prophesize(GeneratorInterface::class);
$generator->generateDocFromTemplate(
$template,
123, // entityId
['key' => 'value'], // contextGenerationData
$storedObject,
$creator
)
->willReturn($storedObject)->shouldBeCalled();
$logger = new NullLogger();
$mailer = $this->prophesize(MailerInterface::class);
$translator = $this->prophesize(TranslatorInterface::class);
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
// Create handler
$handler = new RequestGenerationHandler(
$docGeneratorTemplateRepository->reveal(),
$entityManager->reveal(),
$generator->reveal(),
$logger,
$storedObjectRepository->reveal(),
$userRepository->reveal(),
$mailer->reveal(),
$translator->reveal(),
$storedObjectManager->reveal()
);
// Create message
$message = new RequestGenerationMessage(
$creator,
$template,
123, // entityId
$storedObject,
['key' => 'value'], // contextGenerationData
false, // isTest
null, // sendResultToEmail
false // dumpOnly
);
// Invoke handler
$handler->__invoke($message);
// Assertions
// The assertions are handled by the shouldBeCalled() expectations on the mocks
$this->assertTrue(true); // Just to have an assertion in the test
}
private function setPrivateProperty(object $object, string $propertyName, $value): void
{
$reflection = new \ReflectionClass($object);
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($object, $value);
}
}

View File

@@ -31,6 +31,36 @@ final class DocGenEncoderTest extends TestCase
$this->encoder = new DocGenEncoder();
}
public function testEmbeddedLoopsThrowsException()
{
$this->expectException(UnexpectedValueException::class);
$data = [
'data' => [
['item' => 'one'],
[
'embedded' => [
[
['subitem' => 'two'],
['subitem' => 'three'],
],
],
],
],
];
$this->encoder->encode($data, 'docgen');
}
/**
* @dataProvider generateEncodeData
*/
public function testEncode(mixed $expected, mixed $data, string $msg)
{
$generated = $this->encoder->encode($data, 'docgen');
$this->assertEquals($expected, $generated, $msg);
}
public static function generateEncodeData()
{
yield [['tests' => 'ok'], ['tests' => 'ok'], 'A simple test with a simple array'];
@@ -93,34 +123,4 @@ final class DocGenEncoderTest extends TestCase
'a longer list, with near real data inside and embedded associative arrays',
];
}
public function testEmbeddedLoopsThrowsException()
{
$this->expectException(UnexpectedValueException::class);
$data = [
'data' => [
['item' => 'one'],
[
'embedded' => [
[
['subitem' => 'two'],
['subitem' => 'three'],
],
],
],
],
];
$this->encoder->encode($data, 'docgen');
}
/**
* @dataProvider generateEncodeData
*/
public function testEncode(mixed $expected, mixed $data, string $msg)
{
$generated = $this->encoder->encode($data, 'docgen');
$this->assertEquals($expected, $generated, $msg);
}
}

View File

@@ -1,4 +1,2 @@
docgen:
data_dump_email:
link_valid_until: >-
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
# No ICU messages needed for data_dump_email anymore

View File

@@ -34,8 +34,10 @@ docgen:
data_dump_email:
subject: Contenu des données de génération de document disponible
Dear: Cher
data_dump_ready_and_link: >-
Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant:
data_dump_ready_and_attached: >-
Le contenu des données est disponible. Vous le trouverez en pièce jointe à cet email.
filename: >-
Nom du fichier: %filename%

View File

@@ -85,6 +85,69 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
self::assertEquals($expected, $urlGenerator->validateSignature($signature, $method, $objectName, $expiration), $message);
}
public static function generateValidateSignatureData(): iterable
{
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180),
'HEAD',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid signature',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
false,
'Signature expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name.'____',
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid object name',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
'POST',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Wrong method',
];
}
/**
* @dataProvider generateValidateSignaturePostData
*/
@@ -164,69 +227,6 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
];
}
public static function generateValidateSignatureData(): iterable
{
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180),
'HEAD',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid signature',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
false,
'Signature expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name.'____',
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid object name',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
'POST',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Wrong method',
];
}
private function buildGenerator(?UrlGeneratorInterface $urlGenerator = null, ?ClockInterface $clock = null): TempUrlLocalStorageGenerator
{
return new TempUrlLocalStorageGenerator(

View File

@@ -31,6 +31,20 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
final class StoredObjectManagerTest extends TestCase
{
/**
* @dataProvider getDataProviderForRead
*/
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
public static function getDataProviderForRead(): \Generator
{
/* HAPPY SCENARIO */
@@ -96,6 +110,40 @@ final class StoredObjectManagerTest extends TestCase
];
}
/**
* @dataProvider getDataProviderForWrite
*/
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$previousVersion = $storedObject->getCurrentVersion();
$previousFilename = $previousVersion->getFilename();
$client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) {
self::assertEquals('PUT', $method);
self::assertStringStartsWith('https://example.com/', $url);
self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file');
self::assertArrayHasKey('body', $options);
self::assertEquals($encodedContent, $options['body']);
if (-1 === $errorCode) {
throw new TransportException();
}
return new MockResponse('', ['http_code' => $errorCode ?? 201]);
});
$storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
$newVersion = $storedObjectManager->write($storedObject, $clearContent);
self::assertNotSame($previousVersion, $newVersion);
self::assertSame($storedObject->getCurrentVersion(), $newVersion);
}
public static function getDataProviderForWrite(): \Generator
{
/* HAPPY SCENARIO */
@@ -150,54 +198,6 @@ final class StoredObjectManagerTest extends TestCase
];
}
/**
* @dataProvider getDataProviderForRead
*/
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
/**
* @dataProvider getDataProviderForWrite
*/
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$previousVersion = $storedObject->getCurrentVersion();
$previousFilename = $previousVersion->getFilename();
$client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) {
self::assertEquals('PUT', $method);
self::assertStringStartsWith('https://example.com/', $url);
self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file');
self::assertArrayHasKey('body', $options);
self::assertEquals($encodedContent, $options['body']);
if (-1 === $errorCode) {
throw new TransportException();
}
return new MockResponse('', ['http_code' => $errorCode ?? 201]);
});
$storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
$newVersion = $storedObjectManager->write($storedObject, $clearContent);
self::assertNotSame($previousVersion, $newVersion);
self::assertSame($storedObject->getCurrentVersion(), $newVersion);
}
public function testDelete(): void
{
$storedObject = new StoredObject();

View File

@@ -82,6 +82,38 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertEquals($expected, $signedUrl);
}
public static function dataProviderGenerate(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
$expireDelay = 1800;
$baseUrls = [
'https://objectstore.example/v1/my_account/container/',
'https://objectstore.example/v1/my_account/container',
];
$objectName = 'object';
$method = 'GET';
$key = 'MYKEY';
$signedUrl = new SignedUrl(
'GET',
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'),
$objectName
);
foreach ($baseUrls as $baseUrl) {
yield [
$baseUrl,
$now,
$key,
$method,
$objectName,
$expireDelay,
$signedUrl,
];
}
}
/**
* @dataProvider dataProviderGeneratePost
*/
@@ -125,38 +157,6 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix));
}
public static function dataProviderGenerate(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
$expireDelay = 1800;
$baseUrls = [
'https://objectstore.example/v1/my_account/container/',
'https://objectstore.example/v1/my_account/container',
];
$objectName = 'object';
$method = 'GET';
$key = 'MYKEY';
$signedUrl = new SignedUrl(
'GET',
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'),
$objectName
);
foreach ($baseUrls as $baseUrl) {
yield [
$baseUrl,
$now,
$key,
$method,
$objectName,
$expireDelay,
$signedUrl,
];
}
}
public static function dataProviderGeneratePost(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');

View File

@@ -61,6 +61,55 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
$controller->contentOperate($request);
}
public static function generateOperateContentWithExceptionDataProvider(): iterable
{
yield [
new Request(['object_name' => '', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Object name parameter is missing',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Expiration is not set or equal to zero',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
BadRequestHttpException::class,
'Signature is not set or is a blank string',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
AccessDeniedHttpException::class,
'Invalid signature',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
NotFoundHttpException::class,
'Object does not exists on disk',
false,
'',
true,
];
}
public function testOperateContentGetHappyScenario(): void
{
$objectName = 'testABC';
@@ -286,53 +335,4 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
'Filename does not start with signed prefix',
];
}
public static function generateOperateContentWithExceptionDataProvider(): iterable
{
yield [
new Request(['object_name' => '', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Object name parameter is missing',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Expiration is not set or equal to zero',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
BadRequestHttpException::class,
'Signature is not set or is a blank string',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
AccessDeniedHttpException::class,
'Invalid signature',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
NotFoundHttpException::class,
'Object does not exists on disk',
false,
'',
true,
];
}
}

View File

@@ -136,63 +136,6 @@ class WebdavControllerTest extends KernelTestCase
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
/**
* @dataProvider generateDataPropfindDirectory
*/
public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
{
$controller = $this->buildController();
$request = new Request([], [], [], [], [], [], $requestContent);
$request->setMethod('PROPFIND');
$request->headers->add(['Depth' => '0']);
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
self::assertEquals($expectedStatusCode, $response->getStatusCode());
self::assertContains('content-type', $response->headers->keys());
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
public function testHeadDocument(): void
{
$controller = $this->buildController();
$response = $controller->headDocument($this->buildDocument());
self::assertEquals(200, $response->getStatusCode());
self::assertContains('content-length', $response->headers->keys());
self::assertContains('content-type', $response->headers->keys());
self::assertContains('etag', $response->headers->keys());
self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
self::assertEquals(5, $response->headers->get('content-length'));
}
public function testPutDocument(): void
{
$document = $this->buildDocument();
$entityManager = $this->createMock(EntityManagerInterface::class);
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
// entity manager must be flushed
$entityManager->expects($this->once())
->method('flush');
// object must be written by StoredObjectManager
$storedObjectManager->expects($this->once())
->method('write')
->with($this->identicalTo($document), $this->identicalTo('1234'));
$controller = $this->buildController($entityManager, $storedObjectManager);
$request = new Request(content: '1234');
$response = $controller->putDocument($document, $request);
self::assertEquals(204, $response->getStatusCode());
self::assertEquals('', $response->getContent());
}
public static function generateDataPropfindDocument(): iterable
{
$content =
@@ -347,6 +290,25 @@ class WebdavControllerTest extends KernelTestCase
];
}
/**
* @dataProvider generateDataPropfindDirectory
*/
public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
{
$controller = $this->buildController();
$request = new Request([], [], [], [], [], [], $requestContent);
$request->setMethod('PROPFIND');
$request->headers->add(['Depth' => '0']);
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
self::assertEquals($expectedStatusCode, $response->getStatusCode());
self::assertContains('content-type', $response->headers->keys());
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
public static function generateDataPropfindDirectory(): iterable
{
yield [
@@ -414,6 +376,44 @@ class WebdavControllerTest extends KernelTestCase
'test creatableContentsInfo',
];
}
public function testHeadDocument(): void
{
$controller = $this->buildController();
$response = $controller->headDocument($this->buildDocument());
self::assertEquals(200, $response->getStatusCode());
self::assertContains('content-length', $response->headers->keys());
self::assertContains('content-type', $response->headers->keys());
self::assertContains('etag', $response->headers->keys());
self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
self::assertEquals(5, $response->headers->get('content-length'));
}
public function testPutDocument(): void
{
$document = $this->buildDocument();
$entityManager = $this->createMock(EntityManagerInterface::class);
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
// entity manager must be flushed
$entityManager->expects($this->once())
->method('flush');
// object must be written by StoredObjectManager
$storedObjectManager->expects($this->once())
->method('write')
->with($this->identicalTo($document), $this->identicalTo('1234'));
$controller = $this->buildController($entityManager, $storedObjectManager);
$request = new Request(content: '1234');
$response = $controller->putDocument($document, $request);
self::assertEquals(204, $response->getStatusCode());
self::assertEquals('', $response->getContent());
}
}
class MockedStoredObjectManager implements StoredObjectManagerInterface

View File

@@ -87,6 +87,16 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
self::assertIsInt($nb, 'test that the query could be executed');
}
public static function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new \DateTimeImmutable('1 year ago'), null, null];
yield [null, new \DateTimeImmutable('1 year ago'), null];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
/**
* @dataProvider provideDateForFetchQueryForAccompanyingPeriod
*/
@@ -142,14 +152,4 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
yield [$period, null, null, 'test'];
yield [$period, new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
public static function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new \DateTimeImmutable('1 year ago'), null, null];
yield [null, new \DateTimeImmutable('1 year ago'), null];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
}

View File

@@ -50,19 +50,6 @@ class StoredObjectVoterTest extends TestCase
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
}
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
{
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
->willReturn($supports);
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
->willReturn($voteOnAttribute);
return $storedObjectVoter;
}
public static function provideDataVote(): iterable
{
yield [
@@ -120,4 +107,17 @@ class StoredObjectVoterTest extends TestCase
VoterInterface::ACCESS_GRANTED,
];
}
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
{
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
->willReturn($supports);
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
->willReturn($voteOnAttribute);
return $storedObjectVoter;
}
}

View File

@@ -40,29 +40,6 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
self::assertEquals($expected, $cronJob->canRun($cronJobExecution));
}
public function testRun(): void
{
// we create a clock in the future. This led us a chance to having stored object to delete
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
$repository = $this->createMock(StoredObjectVersionRepository::class);
$repository->expects($this->once())
->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime')
->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00')))
->willReturnCallback(function ($arg) {
yield 1;
yield 3;
yield 2;
})
;
$cronJob = new RemoveOldVersionCronJob($clock, $this->buildMessageBus(true), $repository);
$results = $cronJob->run([]);
self::assertArrayHasKey('last-deleted-stored-object-version-id', $results);
self::assertIsInt($results['last-deleted-stored-object-version-id']);
}
public static function buildTestCanRunData(): iterable
{
yield [
@@ -86,6 +63,29 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
];
}
public function testRun(): void
{
// we create a clock in the future. This led us a chance to having stored object to delete
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
$repository = $this->createMock(StoredObjectVersionRepository::class);
$repository->expects($this->once())
->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime')
->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00')))
->willReturnCallback(function ($arg) {
yield 1;
yield 3;
yield 2;
})
;
$cronJob = new RemoveOldVersionCronJob($clock, $this->buildMessageBus(true), $repository);
$results = $cronJob->run([]);
self::assertArrayHasKey('last-deleted-stored-object-version-id', $results);
self::assertIsInt($results['last-deleted-stored-object-version-id']);
}
private function buildMessageBus(bool $expectDistpatchAtLeastOnce = false): MessageBusInterface
{
$messageBus = $this->createMock(MessageBusInterface::class);

View File

@@ -48,30 +48,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Deletes a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/delete', name: 'chill_eventtype_admin_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_eventtype_admin');
}
/**
* Displays a form to edit an existing EventType entity.
*/
@@ -87,12 +63,10 @@ class EventTypeController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Finds and displays a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/show', name: 'chill_eventtype_admin_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing EventType entity.
*/
@@ -162,7 +114,6 @@ class EventTypeController extends AbstractController
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class EventTypeController extends AbstractController
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,22 +148,6 @@ class EventTypeController extends AbstractController
return $form;
}
/**
* Creates a form to delete a EventType entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl(
'chill_eventtype_admin_delete',
['id' => $id]
))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a EventType entity.
*
@@ -228,7 +162,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,30 +48,6 @@ class RoleController extends AbstractController
]);
}
/**
* Deletes a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/delete', name: 'chill_event_admin_role_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_role');
}
/**
* Displays a form to edit an existing Role entity.
*/
@@ -87,12 +63,10 @@ class RoleController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class RoleController extends AbstractController
]);
}
/**
* Finds and displays a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/show', name: 'chill_event_admin_role_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Role entity.
*/
@@ -162,7 +114,6 @@ class RoleController extends AbstractController
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class RoleController extends AbstractController
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,20 +148,6 @@ class RoleController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Role entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_role_delete', ['id' => $id]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Role entity.
*
@@ -226,7 +162,7 @@ class RoleController extends AbstractController
'chill_event_admin_role_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,30 +48,6 @@ class StatusController extends AbstractController
]);
}
/**
* Deletes a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/delete', name: 'chill_event_admin_status_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_status');
}
/**
* Displays a form to edit an existing Status entity.
*/
@@ -87,12 +63,10 @@ class StatusController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class StatusController extends AbstractController
]);
}
/**
* Finds and displays a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/show', name: 'chill_event_admin_status_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Status entity.
*/
@@ -162,7 +114,6 @@ class StatusController extends AbstractController
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class StatusController extends AbstractController
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,19 +148,6 @@ class StatusController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Status entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Status entity.
*
@@ -222,7 +159,7 @@ class StatusController extends AbstractController
{
$form = $this->createForm(StatusType::class, $entity, [
'action' => $this->generateUrl('chill_event_admin_status_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -8,7 +8,7 @@
{{ form_row(edit_form.name) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -16,14 +16,11 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -8,7 +8,7 @@
{{ form_row(form.name) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -21,17 +21,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -8,12 +8,12 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-edit' }}) }}
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-update' }}) }}
</li>
</ul>

View File

@@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -9,7 +9,7 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -149,7 +149,7 @@ class ExportController extends AbstractController
->createNamedBuilder(
'',
FormType::class,
$defaultFormData,
'centers' === $step ? ['centers' => $defaultFormData] : $defaultFormData,
[
'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST,
'csrf_protection' => !$isGenerate,
@@ -352,7 +352,7 @@ class ExportController extends AbstractController
$formCenters->submit($dataCenters);
$dataAsCollection = $formCenters->getData()['centers'];
$centers = $dataAsCollection['centers'];
$regroupments = $dataAsCollection['regroupments'];
$regroupments = $dataAsCollection['regroupments'] ?? [];
$dataCenters = [
'centers' => $centers instanceof Collection ? $centers->toArray() : $centers,
'regroupments' => $regroupments instanceof Collection ? $regroupments->toArray() : $regroupments,
@@ -377,7 +377,7 @@ class ExportController extends AbstractController
}
return [
'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments'] ?? []],
'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments']],
'export' => $dataExport['export']['export'] ?? [],
'filters' => $dataExport['export']['filters'] ?? [],
'aggregators' => $dataExport['export']['aggregators'] ?? [],
@@ -404,7 +404,12 @@ class ExportController extends AbstractController
/** @var ExportManager $exportManager */
$exportManager = $this->exportManager;
$form = $this->createCreateFormExport($alias, 'centers', [], $savedExport);
$form = $this->createCreateFormExport(
$alias,
'centers',
$this->exportFormHelper->getDefaultData('centers', $export, []),
$savedExport
);
if (Request::METHOD_POST === $request->getMethod()) {
$form->handleRequest($request);

View File

@@ -11,7 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\PermissionsGroup;
use Chill\MainBundle\Entity\RoleScope;
use Chill\MainBundle\Entity\User;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
@@ -62,6 +65,15 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
public function load(ObjectManager $manager): void
{
$roleScope = new RoleScope();
$roleScope->setRole('CHILL_MAIN_COMPOSE_EXPORT');
$permissionGroup = new PermissionsGroup();
$permissionGroup->setName('export');
$permissionGroup->addRoleScope($roleScope);
$manager->persist($roleScope);
$manager->persist($permissionGroup);
foreach (self::$refs as $username => $params) {
$user = new User();
@@ -81,7 +93,14 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username)));
foreach ($params['groupCenterRefs'] as $groupCenterRef) {
$user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class));
$user->addGroupCenter($gc = $this->getReference($groupCenterRef, GroupCenter::class));
$exportGroupCenter = new GroupCenter();
$exportGroupCenter->setPermissionsGroup($permissionGroup);
$exportGroupCenter->setCenter($gc->getCenter());
$manager->persist($exportGroupCenter);
$user->addGroupCenter($exportGroupCenter);
}
echo 'Creating user '.$username."... \n";

View File

@@ -70,9 +70,9 @@ class NewsItem implements TrackCreationInterface, TrackUpdateInterface
return $this->content;
}
public function setContent(string $content): void
public function setContent(?string $content): void
{
$this->content = $content;
$this->content = (string) $content;
}
public function getStartDate(): ?\DateTimeImmutable

View File

@@ -176,6 +176,14 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
]);
}
/**
* Return true if shared with at least one user or one group.
*/
public function isShared(): bool
{
return $this->sharedWithUsers->count() > 0 || $this->sharedWithGroups->count() > 0;
}
/**
* Determines if the user is shared with either directly or through a group.
*

View File

@@ -147,12 +147,12 @@ final readonly class ExportFormHelper
*/
public function getPickedCenters(array $data): array
{
if (!array_key_exists('centers', $data) || !array_key_exists('regroupments', $data)) {
if (!array_key_exists('centers', $data)) {
throw new \RuntimeException('array has not the expected shape');
}
$centers = $data['centers'] instanceof Collection ? $data['centers']->toArray() : $data['centers'];
$regroupments = $data['regroupments'] instanceof Collection ? $data['regroupments']->toArray() : $data['regroupments'];
$regroupments = ($data['regroupments'] ?? []) instanceof Collection ? $data['regroupments']->toArray() : ($data['regroupments'] ?? []);
return $this->centerRegroupementResolver->resolveCenters($regroupments, $centers);
}

View File

@@ -144,11 +144,9 @@ class ExportManager
/**
* @param string $alias
*
* @return AggregatorInterface
*
* @throws \RuntimeException if the aggregator is not known
*/
public function getAggregator($alias)
public function getAggregator($alias): AggregatorInterface
{
if (null === $aggregator = $this->aggregators[$alias] ?? null) {
throw new \RuntimeException("The aggregator with alias {$alias} is not known.");

View File

@@ -1,241 +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\MainBundle\Export\Formatter;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
/**
* Create a CSV List for the export.
*/
class CSVListFormatter implements FormatterInterface, ExportManagerAwareInterface
{
use ExportManagerAwareTrait;
protected $exportAlias;
protected $exportData;
protected $formatterData;
/**
* This variable cache the labels internally.
*
* @var string[]
*/
protected $labelsCache;
protected $result;
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translatorInterface)
{
$this->translator = $translatorInterface;
}
/**
* build a form, which will be used to collect data required for the execution
* of this formatter.
*
* @uses appendAggregatorForm
*
* @param type $exportAlias
*/
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases,
): void {
$builder->add('numerotation', ChoiceType::class, [
'choices' => [
'yes' => true,
'no' => false,
],
'expanded' => true,
'multiple' => false,
'label' => 'Add a number on first column',
]);
}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return ['numerotation' => $formData['numerotation']];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return ['numerotation' => $formData['numerotation']];
}
public function getFormDefaultData(array $aggregatorAliases): array
{
return ['numerotation' => true];
}
public function getName(): string|TranslatableInterface
{
return 'CSV vertical list';
}
/**
* Generate a response from the data collected on differents ExportElementInterface.
*
* @param mixed[] $result The result, as given by the ExportInterface
* @param mixed[] $formatterData collected from the current form
* @param string $exportAlias the id of the current export
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
*
* @return Response The response to be shown
*/
public function getResponse(
$result,
$formatterData,
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
ExportGenerationContext $context,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;
$this->exportData = $exportData;
$this->formatterData = $formatterData;
$output = fopen('php://output', 'wb');
$this->prepareHeaders($output);
$i = 1;
foreach ($result as $row) {
$line = [];
if (true === $this->formatterData['numerotation']) {
$line[] = $i;
}
foreach ($row as $key => $value) {
$line[] = $this->getLabel($key, $value);
}
fputcsv($output, $line);
++$i;
}
$csvContent = stream_get_contents($output);
fclose($output);
$response = new Response();
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
// $response->headers->set('Content-Disposition','attachment; filename="export.csv"');
$response->setContent($csvContent);
return $response;
}
public function getType(): string
{
return FormatterInterface::TYPE_LIST;
}
/**
* Give the label corresponding to the given key and value.
*
* @param string $key
* @param string $value
*
* @throws \LogicException if the label is not found
*/
protected function getLabel($key, $value)
{
if (null === $this->labelsCache) {
$this->prepareCacheLabels();
}
if (!\array_key_exists($key, $this->labelsCache)) {
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
}
return $this->labelsCache[$key]($value);
}
/**
* Prepare the label cache which will be used by getLabel. This function
* should be called only once in the generation lifecycle.
*/
protected function prepareCacheLabels()
{
$export = $this->getExportManager()->getExport($this->exportAlias);
$keys = $export->getQueryKeys($this->exportData);
foreach ($keys as $key) {
// get an array with all values for this key if possible
$values = \array_map(static fn ($v) => $v[$key], $this->result);
// store the label in the labelsCache property
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
}
}
/**
* add the headers to the csv file.
*
* @param resource $output
*/
protected function prepareHeaders($output)
{
$keys = $this->getExportManager()->getExport($this->exportAlias)->getQueryKeys($this->exportData);
// we want to keep the order of the first row. So we will iterate on the first row of the results
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
$header_line = [];
if (true === $this->formatterData['numerotation']) {
$header_line[] = $this->translator->trans('Number');
}
foreach ($first_row as $key => $value) {
$content = $this->getLabel($key, '_header');
if ($content instanceof TranslatableInterface) {
$header_line[] = $content->trans($this->translator, $this->translator->getLocale());
} else {
$header_line[] = $this->translator->trans($this->getLabel($key, '_header'));
}
}
if (\count($header_line) > 0) {
fputcsv($output, $header_line);
}
}
}

View File

@@ -1,229 +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\MainBundle\Export\Formatter;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Create a CSV List for the export where the header are printed on the
* first column, and the result goes from left to right.
*/
class CSVPivotedListFormatter implements FormatterInterface, ExportManagerAwareInterface
{
use ExportManagerAwareTrait;
protected $exportAlias;
protected $exportData;
protected $formatterData;
/**
* This variable cache the labels internally.
*
* @var string[]
*/
protected $labelsCache;
protected $result;
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translatorInterface)
{
$this->translator = $translatorInterface;
}
/**
* build a form, which will be used to collect data required for the execution
* of this formatter.
*
* @uses appendAggregatorForm
*
* @param type $exportAlias
*/
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases,
): void {
$builder->add('numerotation', ChoiceType::class, [
'choices' => [
'yes' => true,
'no' => false,
],
'expanded' => true,
'multiple' => false,
'label' => 'Add a number on first column',
'data' => true,
]);
}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return ['numerotation' => $formData['numerotation']];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return ['numerotation' => $formData['numerotation']];
}
public function getFormDefaultData(array $aggregatorAliases): array
{
return ['numerotation' => true];
}
public function getName(): string|\Symfony\Contracts\Translation\TranslatableInterface
{
return 'CSV horizontal list';
}
/**
* Generate a response from the data collected on differents ExportElementInterface.
*
* @param mixed[] $result The result, as given by the ExportInterface
* @param mixed[] $formatterData collected from the current form
* @param string $exportAlias the id of the current export
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
*
* @return Response The response to be shown
*/
public function getResponse(
$result,
$formatterData,
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
ExportGenerationContext $context,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;
$this->exportData = $exportData;
$this->formatterData = $formatterData;
$output = fopen('php://output', 'wb');
$i = 1;
$lines = [];
$this->prepareHeaders($lines);
foreach ($result as $row) {
$j = 0;
if (true === $this->formatterData['numerotation']) {
$lines[$j][] = $i;
++$j;
}
foreach ($row as $key => $value) {
$lines[$j][] = $this->getLabel($key, $value);
++$j;
}
++$i;
}
// adding the lines to the csv output
foreach ($lines as $line) {
fputcsv($output, $line);
}
$csvContent = stream_get_contents($output);
fclose($output);
$response = new Response();
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
$response->setContent($csvContent);
return $response;
}
public function getType(): string
{
return FormatterInterface::TYPE_LIST;
}
/**
* Give the label corresponding to the given key and value.
*
* @param string $key
* @param string $value
*
* @throws \LogicException if the label is not found
*/
protected function getLabel($key, $value)
{
if (null === $this->labelsCache) {
$this->prepareCacheLabels();
}
return $this->labelsCache[$key]($value);
}
/**
* Prepare the label cache which will be used by getLabel. This function
* should be called only once in the generation lifecycle.
*/
protected function prepareCacheLabels()
{
$export = $this->getExportManager()->getExport($this->exportAlias);
$keys = $export->getQueryKeys($this->exportData);
foreach ($keys as $key) {
// get an array with all values for this key if possible
$values = \array_map(static fn ($v) => $v[$key], $this->result);
// store the label in the labelsCache property
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
}
}
/**
* add the headers to lines array.
*
* @param array $lines the lines where the header will be added
*/
protected function prepareHeaders(array &$lines)
{
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
// we want to keep the order of the first row. So we will iterate on the first row of the results
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
$header_line = [];
if (true === $this->formatterData['numerotation']) {
$lines[] = [$this->translator->trans('Number')];
}
foreach ($first_row as $key => $value) {
$lines[] = [$this->getLabel($key, '_header')];
}
}
}

View File

@@ -12,110 +12,25 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export\Formatter;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormattedExportGeneration;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface
final class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface
{
use ExportManagerAwareTrait;
/**
* an array where keys are the aggregators aliases and
* values are the data.
*
* replaced when `getResponse` is called.
*/
protected array $aggregatorsData;
/**
* The export.
*
* replaced when `getResponse` is called.
*
* @var \Chill\MainBundle\Export\ExportInterface
*/
protected $export;
/**
* array containing value of export form.
*
* replaced when `getResponse` is called.
*
* @var array
*/
protected $exportData;
/**
* replaced when `getResponse` is called.
*
* @var array
*/
protected $filtersData;
/**
* replaced when `getResponse` is called.
*
* @var array
*/
protected $formatterData;
/**
* The result, as returned by the export.
*
* replaced when `getResponse` is called.
*
* @var array
*/
protected $result;
/**
* replaced when `getResponse` is called.
*
* @var array
*/
// protected $labels;
/**
* temporary file to store spreadsheet.
*
* @var string
*/
protected $tempfile;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* cache for displayable result.
*
* This cache is reset when `getResponse` is called.
*
* The array's keys are the keys in the raw result, and
* values are the callable which will transform the raw result to
* displayable result.
*/
private ?array $cacheDisplayableResult = null;
/**
* Whethe `cacheDisplayableResult` is initialized or not.
*/
private bool $cacheDisplayableResultIsInitialized = false;
public function __construct(TranslatorInterface $translatorInterface)
{
$this->translator = $translatorInterface;
}
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(
FormBuilderInterface $builder,
@@ -178,6 +93,51 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
return 'SpreadSheet (xlsx, ods)';
}
public function generate(
$result,
$formatterData,
string $exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
ExportGenerationContext $context,
) {
// Initialize local variables instead of class properties
/** @var ExportInterface $export */
$export = $this->getExportManager()->getExport($exportAlias);
// Initialize cache variables
$cacheDisplayableResult = $this->initializeDisplayable($result, $export, $exportData, $aggregatorsData);
$tempfile = \tempnam(\sys_get_temp_dir(), '');
if (false === $tempfile) {
throw new \RuntimeException('Unable to create temporary file');
}
$this->generateContent(
$context,
$tempfile,
$result,
$formatterData,
$export,
$exportData,
$filtersData,
$aggregatorsData,
$cacheDisplayableResult,
);
$result = new FormattedExportGeneration(
file_get_contents($tempfile),
$this->getContentType($formatterData['format']),
);
// remove the temp file from disk
\unlink($tempfile);
return $result;
}
public function getResponse(
$result,
$formatterData,
@@ -187,33 +147,10 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
array $aggregatorsData,
ExportGenerationContext $context,
): Response {
// store all data when the process is initiated
$this->result = $result;
$this->formatterData = $formatterData;
$this->export = $this->getExportManager()->getExport($exportAlias);
$this->exportData = $exportData;
$this->filtersData = $filtersData;
$this->aggregatorsData = $aggregatorsData;
$formattedResult = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
// reset cache
$this->cacheDisplayableResult = [];
$this->cacheDisplayableResultIsInitialized = false;
$response = new Response();
$response->headers->set(
'Content-Type',
$this->getContentType($this->formatterData['format'])
);
$this->tempfile = \tempnam(\sys_get_temp_dir(), '');
$this->generateContent($context);
$f = \fopen($this->tempfile, 'rb');
$response->setContent(\stream_get_contents($f));
fclose($f);
// remove the temp file from disk
\unlink($this->tempfile);
$response = new BinaryFileResponse($formattedResult->content);
$response->headers->set('Content-Type', $formattedResult->contentType);
return $response;
}
@@ -223,7 +160,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
return 'tabular';
}
protected function addContentTable(
private function addContentTable(
Worksheet $worksheet,
$sortedResults,
$line,
@@ -245,11 +182,11 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
*
* @return int the line number after the last description
*/
protected function addFiltersDescription(Worksheet &$worksheet, ExportGenerationContext $context)
private function addFiltersDescription(Worksheet &$worksheet, ExportGenerationContext $context, array $filtersData)
{
$line = 3;
foreach ($this->filtersData as $alias => $data) {
foreach ($filtersData as $alias => $data) {
$filter = $this->getExportManager()->getFilter($alias);
$description = $filter->describeAction($data, $context);
if (\is_array($description)) {
@@ -274,26 +211,22 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
*
* return the line number where the next content (i.e. result) should
* be appended.
*
* @param int $line
*
* @return int
*/
protected function addHeaders(
private function addHeaders(
Worksheet &$worksheet,
array $globalKeys,
$line,
) {
int $line,
array $cacheDisplayableResult = [],
): int {
// get the displayable form of headers
$displayables = [];
foreach ($globalKeys as $key) {
$displayable = $this->getDisplayableResult($key, '_header');
$displayable = $this->getDisplayableResult($key, '_header', $cacheDisplayableResult);
if ($displayable instanceof TranslatableInterface) {
$displayables[] = $displayable->trans($this->translator, $this->translator->getLocale());
} else {
$displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header'));
$displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header', $cacheDisplayableResult));
}
}
@@ -311,9 +244,9 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
* Add the title to the worksheet and merge the cell containing
* the title.
*/
protected function addTitleToWorkSheet(Worksheet &$worksheet)
private function addTitleToWorkSheet(Worksheet &$worksheet, $export)
{
$worksheet->setCellValue('A1', $this->getTitle());
$worksheet->setCellValue('A1', $this->getTitle($export));
$worksheet->mergeCells('A1:G1');
}
@@ -322,14 +255,14 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
*
* @return array where 1st member is spreadsheet, 2nd is worksheet
*/
protected function createSpreadsheet()
private function createSpreadsheet($export)
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
// setting the worksheet title and code name
$worksheet
->setTitle($this->getTitle())
->setTitle($this->getTitle($export))
->setCodeName('result');
return [$spreadsheet, $worksheet];
@@ -338,29 +271,38 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
/**
* Generate the content and write it to php://temp.
*/
protected function generateContent(ExportGenerationContext $context)
{
[$spreadsheet, $worksheet] = $this->createSpreadsheet();
private function generateContent(
ExportGenerationContext $context,
string $tempfile,
$result,
$formatterData,
$export,
array $exportData,
array $filtersData,
array $aggregatorsData,
array $cacheDisplayableResult,
) {
[$spreadsheet, $worksheet] = $this->createSpreadsheet($export);
$this->addTitleToWorkSheet($worksheet);
$line = $this->addFiltersDescription($worksheet, $context);
$this->addTitleToWorkSheet($worksheet, $export);
$line = $this->addFiltersDescription($worksheet, $context, $filtersData);
// at this point, we are going to sort retsults for an easier manipulation
// at this point, we are going to sort results for an easier manipulation
[$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys] =
$this->sortResult();
$this->sortResult($result, $export, $exportData, $aggregatorsData, $formatterData, $cacheDisplayableResult);
$line = $this->addHeaders($worksheet, $globalKeys, $line);
$line = $this->addHeaders($worksheet, $globalKeys, $line, $cacheDisplayableResult);
$line = $this->addContentTable($worksheet, $sortedResult, $line);
$this->addContentTable($worksheet, $sortedResult, $line);
$writer = match ($this->formatterData['format']) {
$writer = match ($formatterData['format']) {
'ods' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'),
'xlsx' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx'),
'csv' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv'),
default => throw new \LogicException(),
};
$writer->save($this->tempfile);
$writer->save($tempfile);
}
/**
@@ -369,7 +311,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
*
* @return string[] an array containing the keys of aggregators
*/
protected function getAggregatorKeysSorted()
private function getAggregatorKeysSorted(array $aggregatorsData, array $formatterData)
{
// empty array for aggregators keys
$keys = [];
@@ -377,7 +319,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
// during sorting
$aggregatorKeyAssociation = [];
foreach ($this->aggregatorsData as $alias => $data) {
foreach ($aggregatorsData as $alias => $data) {
$aggregator = $this->exportManager->getAggregator($alias);
$aggregatorsKeys = $aggregator->getQueryKeys($data);
// append the keys from aggregator to the $keys existing array
@@ -389,9 +331,9 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
}
// sort the result using the form
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation) {
$A = $this->formatterData[$aggregatorKeyAssociation[$a]]['order'];
$B = $this->formatterData[$aggregatorKeyAssociation[$b]]['order'];
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation, $formatterData) {
$A = $formatterData[$aggregatorKeyAssociation[$a]]['order'];
$B = $formatterData[$aggregatorKeyAssociation[$b]]['order'];
if ($A === $B) {
return 0;
@@ -407,7 +349,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
return $keys;
}
protected function getContentType($format)
private function getContentType($format)
{
switch ($format) {
case 'csv':
@@ -424,23 +366,20 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
/**
* Get the displayable result.
*
* @param string $key
*/
protected function getDisplayableResult($key, mixed $value)
{
if (false === $this->cacheDisplayableResultIsInitialized) {
$this->initializeCache($key);
}
private function getDisplayableResult(
string $key,
mixed $value,
array $cacheDisplayableResult,
): string|TranslatableInterface|\DateTimeInterface|int|float|bool {
$value ??= '';
return \call_user_func($this->cacheDisplayableResult[$key], $value);
return \call_user_func($cacheDisplayableResult[$key], $value);
}
protected function getTitle(): string
private function getTitle($export): string
{
$original = $this->export->getTitle();
$original = $export->getTitle();
if ($original instanceof TranslatableInterface) {
$title = $original->trans($this->translator, $this->translator->getLocale());
@@ -455,8 +394,13 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
return $title;
}
protected function initializeCache($key)
{
private function initializeDisplayable(
$result,
ExportInterface $export,
array $exportData,
array $aggregatorsData,
): array {
$cacheDisplayableResult = [];
/*
* this function follows the following steps :
*
@@ -469,12 +413,11 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
// 1. create an associative array with key and export / aggregator
$keysExportElementAssociation = [];
// keys for export
foreach ($this->export->getQueryKeys($this->exportData) as $key) {
$keysExportElementAssociation[$key] = [$this->export,
$this->exportData, ];
foreach ($export->getQueryKeys($exportData) as $key) {
$keysExportElementAssociation[$key] = [$export, $exportData];
}
// keys for aggregator
foreach ($this->aggregatorsData as $alias => $data) {
foreach ($aggregatorsData as $alias => $data) {
$aggregator = $this->getExportManager()->getAggregator($alias);
foreach ($aggregator->getQueryKeys($data) as $key) {
@@ -487,7 +430,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
$allValues = [];
// store all the values in an array
foreach ($this->result as $row) {
foreach ($result as $row) {
foreach ($keys as $key) {
$allValues[$key][] = $row[$key];
}
@@ -498,15 +441,14 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
foreach ($keysExportElementAssociation as $key => [$element, $data]) {
// handle the case when there is not results lines (query is empty)
if ([] === $allValues) {
$this->cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
$cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
} else {
$this->cacheDisplayableResult[$key] =
$cacheDisplayableResult[$key] =
$element->getLabels($key, \array_unique($allValues[$key]), $data);
}
}
// the cache is initialized !
$this->cacheDisplayableResultIsInitialized = true;
return $cacheDisplayableResult;
}
/**
@@ -544,23 +486,28 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
* )
* ```
*/
protected function sortResult()
{
private function sortResult(
$result,
ExportInterface $export,
array $exportData,
array $aggregatorsData,
array $formatterData,
array $cacheDisplayableResult,
) {
// get the keys for each row
$exportKeys = $this->export->getQueryKeys($this->exportData);
$aggregatorKeys = $this->getAggregatorKeysSorted();
$exportKeys = $export->getQueryKeys($exportData);
$aggregatorKeys = $this->getAggregatorKeysSorted($aggregatorsData, $formatterData);
$globalKeys = \array_merge($aggregatorKeys, $exportKeys);
$sortedResult = \array_map(function ($row) use ($globalKeys) {
$sortedResult = \array_map(function ($row) use ($globalKeys, $cacheDisplayableResult) {
$newRow = [];
foreach ($globalKeys as $key) {
$newRow[] = $this->getDisplayableResult($key, $row[$key]);
$newRow[] = $this->getDisplayableResult($key, $row[$key], $cacheDisplayableResult);
}
return $newRow;
}, $this->result);
}, $result);
\array_multisort($sortedResult);

View File

@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Export\Formatter;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormattedExportGeneration;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use PhpOffice\PhpSpreadsheet\Shared\Date;
@@ -21,7 +22,9 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@@ -31,42 +34,17 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
{
use ExportManagerAwareTrait;
protected $exportAlias;
protected $exportData;
protected $formatterData;
/**
* This variable cache the labels internally.
*
* @var string[]
*/
protected $labelsCache;
protected $result;
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translatorInterface)
{
$this->translator = $translatorInterface;
}
public function __construct(private readonly TranslatorInterface $translator) {}
/**
* build a form, which will be used to collect data required for the execution
* of this formatter.
*
* @uses appendAggregatorForm
*
* @param string $exportAlias
*/
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
string $exportAlias,
array $aggregatorAliases,
): void {
$builder
@@ -108,54 +86,32 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
return ['numerotation' => true, 'format' => 'xlsx'];
}
public function getName(): string|\Symfony\Contracts\Translation\TranslatableInterface
public function getName(): string|TranslatableInterface
{
return 'Spreadsheet list formatter (.xlsx, .ods)';
}
/**
* Generate a response from the data collected on differents ExportElementInterface.
*
* @param mixed[] $result The result, as given by the ExportInterface
* @param mixed[] $formatterData collected from the current form
* @param string $exportAlias the id of the current export
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
*
* @return Response The response to be shown
*/
public function getResponse(
$result,
$formatterData,
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
ExportGenerationContext $context,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;
$this->exportData = $exportData;
$this->formatterData = $formatterData;
public function generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$cacheLabels = $this->prepareCacheLabels($result, $exportAlias, $exportData);
$this->prepareHeaders($worksheet);
$this->prepareHeaders($cacheLabels, $worksheet, $result, $formatterData, $exportAlias, $exportData);
$i = 1;
foreach ($result as $row) {
if (true === $this->formatterData['numerotation']) {
if (true === $formatterData['numerotation']) {
$worksheet->setCellValue('A'.($i + 1), (string) $i);
}
$a = $this->formatterData['numerotation'] ? 'B' : 'A';
$a = $formatterData['numerotation'] ? 'B' : 'A';
foreach ($row as $key => $value) {
$row = $a.($i + 1);
$formattedValue = $this->getLabel($key, $value);
$formattedValue = $this->getLabel($cacheLabels, $key, $value, $result, $exportAlias, $exportData);
if ($formattedValue instanceof \DateTimeInterface) {
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
@@ -169,6 +125,8 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
}
} elseif ($formattedValue instanceof TranslatableInterface) {
$worksheet->setCellValue($row, $formattedValue->trans($this->translator));
} else {
$worksheet->setCellValue($row, $formattedValue);
}
@@ -178,7 +136,7 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
++$i;
}
switch ($this->formatterData['format']) {
switch ($formatterData['format']) {
case 'ods':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods');
$contentType = 'application/vnd.oasis.opendocument.spreadsheet';
@@ -201,22 +159,48 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
default:
// this should not happen
// throw an exception to ensure that the error is catched
throw new \OutOfBoundsException('The format '.$this->formatterData['format'].' is not supported');
throw new \OutOfBoundsException('The format '.$formatterData['format'].' is not supported');
}
$response = new Response();
$response->headers->set('content-type', $contentType);
$tempfile = \tempnam(\sys_get_temp_dir(), '');
$writer->save($tempfile);
$f = \fopen($tempfile, 'rb');
$response->setContent(\stream_get_contents($f));
fclose($f);
$generated = new FormattedExportGeneration(
file_get_contents($tempfile),
$contentType,
);
// remove the temp file from disk
\unlink($tempfile);
return $generated;
}
/**
* Generate a response from the data collected on differents ExportElementInterface.
*
* @param mixed[] $result The result, as given by the ExportInterface
* @param mixed[] $formatterData collected from the current form
* @param string $exportAlias the id of the current export
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
*
* @return Response The response to be shown
*/
public function getResponse(
$result,
$formatterData,
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
ExportGenerationContext $context,
) {
$generated = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
$response = new BinaryFileResponse($generated->content);
$response->headers->set('Content-Type', $generated->contentType);
return $response;
}
@@ -228,34 +212,29 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
/**
* Give the label corresponding to the given key and value.
*
* @param string $key
* @param string $value
*
* @return string
* @return string|\DateTimeInterface|int|float|TranslatableInterface|null
*
* @throws \LogicException if the label is not found
*/
protected function getLabel($key, $value)
private function getLabel(array $labelsCache, $key, $value, array $result, string $exportAlias, array $exportData)
{
if (null === $this->labelsCache) {
$this->prepareCacheLabels();
if (!\array_key_exists($key, $labelsCache)) {
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($labelsCache))));
}
if (!\array_key_exists($key, $this->labelsCache)) {
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
}
return $this->labelsCache[$key]($value);
return $labelsCache[$key]($value);
}
/**
* Prepare the label cache which will be used by getLabel. This function
* should be called only once in the generation lifecycle.
* Prepare the label cache which will be used by getLabel.
*
* @return array The labels cache
*/
protected function prepareCacheLabels()
private function prepareCacheLabels(array $result, string $exportAlias, array $exportData): array
{
$export = $this->getExportManager()->getExport($this->exportAlias);
$keys = $export->getQueryKeys($this->exportData);
$labelsCache = [];
$export = $this->getExportManager()->getExport($exportAlias);
$keys = $export->getQueryKeys($exportData);
foreach ($keys as $key) {
// get an array with all values for this key if possible
@@ -265,29 +244,31 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
}
return $v[$key];
}, $this->result);
// store the label in the labelsCache property
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
}, $result);
// store the label in the labelsCache
$labelsCache[$key] = $export->getLabels($key, $values, $exportData);
}
return $labelsCache;
}
/**
* add the headers to the csv file.
*/
protected function prepareHeaders(Worksheet $worksheet)
protected function prepareHeaders(array $labelsCache, Worksheet $worksheet, array $result, array $formatterData, string $exportAlias, array $exportData)
{
$keys = $this->getExportManager()->getExport($this->exportAlias)->getQueryKeys($this->exportData);
$keys = $this->getExportManager()->getExport($exportAlias)->getQueryKeys($exportData);
// we want to keep the order of the first row. So we will iterate on the first row of the results
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
$first_row = \count($result) > 0 ? $result[0] : [];
$header_line = [];
if (true === $this->formatterData['numerotation']) {
if (true === $formatterData['numerotation']) {
$header_line[] = $this->translator->trans('Number');
}
foreach ($first_row as $key => $value) {
$header_line[] = $this->translator->trans(
$this->getLabel($key, '_header')
$this->getLabel($labelsCache, $key, '_header', $result, $exportAlias, $exportData)
);
}

View File

@@ -20,7 +20,7 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
#[AsMessageHandler]
class RemoveExportGenerationMessageHandler implements MessageHandlerInterface
final readonly class RemoveExportGenerationMessageHandler implements MessageHandlerInterface
{
private const LOG_PREFIX = '[RemoveExportGenerationMessageHandler] ';

View File

@@ -202,10 +202,6 @@ export interface WorkflowAttachment {
genericDoc: null | GenericDoc;
}
export interface PrivateCommentEmbeddable {
comments: Record<number, string>;
}
export interface ExportGeneration {
id: string;
type: "export_generation";
@@ -215,3 +211,7 @@ export interface ExportGeneration {
status: StoredObjectStatus;
storedObject: StoredObject;
}
export interface PrivateCommentEmbeddable {
comments: Record<number, string>;
}

View File

@@ -1,11 +1,11 @@
<template>
<span class="chill-entity entity-user">
{{ user.label }}
<span class="user-job" v-if="user.user_job !== null"
> ({{ localizeString(user.user_job.label) }})</span
<span class="user-job" v-if="user.user_job !== null">
({{ localizeString(user.user_job.label) }})</span
>
<span class="main-scope" v-if="user.main_scope !== null"
> ({{ localizeString(user.main_scope.name) }})</span
<span class="main-scope" v-if="user.main_scope !== null">
({{ localizeString(user.main_scope.name) }})</span
>
<span
v-if="user.isAbsent"

View File

@@ -4,7 +4,7 @@
{% endblock crud_content_header %}
{% block crud_content_view %}
{% block crud_content_view_details %}
<dl class="chill_view_data">
<dt>id</dt>
@@ -20,7 +20,7 @@
{{ 'Cancel'|trans }}
</a>
</li>
{% endblock %}
{% endblock %}
{% block content_view_actions_before %}{% endblock %}
{% block content_form_actions_delete %}
{% if chill_crud_action_exists(crud_name, 'delete') %}
@@ -32,7 +32,7 @@
</li>
{% endif %}
{% endif %}
{% endblock content_form_actions_delete %}
{% endblock content_form_actions_delete %}
{% block content_view_actions_duplicate_link %}
{% if chill_crud_action_exists(crud_name, 'new') %}
{% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %}
@@ -44,17 +44,6 @@
{% endif %}
{% endif %}
{% endblock content_view_actions_duplicate_link %}
{% block content_view_actions_merge %}
<li>
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
{ 'thirdparty_id': entity.id }) }}"
title="{{ 'Merge'|trans }}"
class="btn btn-misc">
<i class="bi bi-chevron-contract"></i>
{{ 'Merge'|trans }}
</a>
</li>
{% endblock %}
{% block content_view_actions_edit_link %}
{% if chill_crud_action_exists(crud_name, 'edit') %}
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}

View File

@@ -30,6 +30,7 @@
{% if app.user is same as saved.user %}
<p class="card-text tags">
{% if app.user is same as saved.user %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
{% if saved.isShared() %}<span class="badge bg-info">{{ 'saved_export.Shared with others'|trans }}</span>{% endif %}
</p>
{% else %}
<p class="card-text tags">

View File

@@ -38,7 +38,10 @@ final class SavedExportVoter extends Voter
self::DUPLICATE,
];
public function __construct(private readonly ExportManager $exportManager, private readonly AccessDecisionManagerInterface $accessDecisionManager) {}
public function __construct(
private readonly ExportManager $exportManager,
private readonly AccessDecisionManagerInterface $accessDecisionManager,
) {}
protected function supports($attribute, $subject): bool
{
@@ -55,7 +58,8 @@ final class SavedExportVoter extends Voter
}
return match ($attribute) {
self::DELETE, self::EDIT, self::SHARE => $subject->getUser() === $token->getUser(),
self::DELETE, self::EDIT => $subject->getUser() === $token->getUser(),
self::SHARE => $subject->getUser() === $token->getUser() && $this->accessDecisionManager->decide($token, [ChillExportVoter::COMPOSE_EXPORT]),
self::DUPLICATE => $this->accessDecisionManager->decide($token, [ChillExportVoter::COMPOSE_EXPORT]) && $this->accessDecisionManager->decide($token, [self::EDIT], $subject) ,
self::GENERATE => $this->canUserGenerate($user, $subject),
default => throw new \UnexpectedValueException('attribute not supported: '.$attribute),

View File

@@ -58,7 +58,6 @@ final readonly class CancelStaleWorkflowHandler
$transitions = $workflowComponent->getEnabledTransitions($workflow);
$transitionApplied = false;
$wasInInitialPosition = 'initial' === $workflow->getStep();
foreach ($transitions as $transition) {
if ($this->willTransitionLeadToFinalNegative($transition, $metadataStore)) {
@@ -80,10 +79,6 @@ final readonly class CancelStaleWorkflowHandler
throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
}
if ($wasInInitialPosition) {
$this->em->remove($workflow);
}
$this->em->flush();
}

View File

@@ -34,119 +34,6 @@ abstract class AbstractAggregatorTest extends KernelTestCase
self::ensureKernelShutdown();
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
/**
* provide data for `testAlterQuery`.
*/
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderQueryExecution(): iterable
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* prepare data for `testGetQueryKeys`.
*/
public static function dataProviderGetQueryKeys()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach ($datas as $data) {
yield [$data];
}
}
/**
* prepare date for method `testGetResultsAndLabels`.
*/
public static function dataProviderGetResultsAndLabels()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* Create an aggregator instance which will be used in tests.
*
@@ -214,6 +101,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
}
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* @dataProvider dataProviderQueryExecution
*
@@ -228,6 +137,25 @@ abstract class AbstractAggregatorTest extends KernelTestCase
self::assertIsArray($actual);
}
public static function dataProviderQueryExecution(): iterable
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the alteration of query by the filter.
*
@@ -266,6 +194,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
);
}
/**
* provide data for `testAlterQuery`.
*/
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* Test the `applyOn` method.
*/
@@ -309,6 +259,18 @@ abstract class AbstractAggregatorTest extends KernelTestCase
}
}
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
/**
* Test that the query keys are strings.
*
@@ -335,6 +297,22 @@ abstract class AbstractAggregatorTest extends KernelTestCase
);
}
/**
* prepare data for `testGetQueryKeys`.
*/
public static function dataProviderGetQueryKeys()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach ($datas as $data) {
yield [$data];
}
}
/**
* Test that.
*
@@ -399,6 +377,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
}
}
/**
* prepare date for method `testGetResultsAndLabels`.
*/
public static function dataProviderGetResultsAndLabels()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the `getTitle` method.
*/

View File

@@ -33,39 +33,6 @@ abstract class AbstractExportTest extends WebTestCase
{
use PrepareClientTrait;
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
public static function dataProviderGetQueryKeys()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
/**
* create data for `ìnitiateQuery` method.
*/
public static function dataProviderInitiateQuery()
{
$acl = static::getAcl();
foreach (static::getModifiersCombination() as $modifiers) {
foreach (static::getFormData() as $data) {
yield [$modifiers, $acl, $data];
}
}
}
/**
* Return an array usable as ACL.
*
@@ -182,6 +149,18 @@ abstract class AbstractExportTest extends WebTestCase
}
}
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
private function testOneDataNormalization(ExportInterface|DirectExportInterface $export, array $data, int $version, array $customAssert): void
{
$normalized = $export->normalizeFormData($data);
@@ -262,6 +241,13 @@ abstract class AbstractExportTest extends WebTestCase
}
}
public static function dataProviderGetQueryKeys()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
/**
* Test that.
*
@@ -446,4 +432,18 @@ abstract class AbstractExportTest extends WebTestCase
}
}
}
/**
* create data for `ìnitiateQuery` method.
*/
public static function dataProviderInitiateQuery()
{
$acl = static::getAcl();
foreach (static::getModifiersCombination() as $modifiers) {
foreach (static::getFormData() as $data) {
yield [$modifiers, $acl, $data];
}
}
}
}

View File

@@ -35,61 +35,6 @@ abstract class AbstractFilterTest extends KernelTestCase
self::ensureKernelShutdown();
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProvideQueryExecution(): iterable
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderDescriptionAction()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
protected function getUser(): User
{
$em = static::getContainer()->get(EntityManagerInterface::class);
@@ -134,18 +79,6 @@ abstract class AbstractFilterTest extends KernelTestCase
*/
abstract public static function getQueryBuilders();
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
/**
* @dataProvider dataProviderFormDataToNormalize
*/
@@ -172,6 +105,18 @@ abstract class AbstractFilterTest extends KernelTestCase
}
}
/**
* A list of data to normalize.
*
* @return iterable{array}
*/
public static function dataProviderFormDataToNormalize(): iterable
{
foreach (static::getFormData() as $data) {
yield [$data, 1, []];
}
}
/**
* Compare aliases array before and after that filter alter query.
*
@@ -192,6 +137,24 @@ abstract class AbstractFilterTest extends KernelTestCase
}
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the alteration of query by the filter.
*
@@ -232,6 +195,21 @@ abstract class AbstractFilterTest extends KernelTestCase
);
}
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* @dataProvider dataProvideQueryExecution
*/
@@ -244,6 +222,21 @@ abstract class AbstractFilterTest extends KernelTestCase
self::assertIsArray($actual);
}
public static function dataProvideQueryExecution(): iterable
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public function testApplyOn()
{
$filter = $this->getFilter();
@@ -308,4 +301,11 @@ abstract class AbstractFilterTest extends KernelTestCase
);
}
}
public static function dataProviderDescriptionAction()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
}

View File

@@ -32,6 +32,17 @@ final class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\W
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAddressIds
*/
public function testDuplicate(int $addressId)
{
$this->client = $this->getClientAuthenticated();
$this->client->request('POST', "/api/1.0/main/address/{$addressId}/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
public static function generateAddressIds(): iterable
{
self::bootKernel();
@@ -49,15 +60,4 @@ final class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\W
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAddressIds
*/
public function testDuplicate(int $addressId)
{
$this->client = $this->getClientAuthenticated();
$this->client->request('POST', "/api/1.0/main/address/{$addressId}/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
}

View File

@@ -25,6 +25,22 @@ final class AddressReferenceApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider provideData
*/
public function testSearch(int $postCodeId, string $pattern)
{
$client = $this->getClientAuthenticated();
$client->request(
'GET',
"/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
['q' => $pattern]
);
$this->assertResponseIsSuccessful();
}
public static function provideData()
{
self::bootKernel();
@@ -42,20 +58,4 @@ final class AddressReferenceApiControllerTest extends WebTestCase
yield [$postalCode->getId(), 'rue'];
}
/**
* @dataProvider provideData
*/
public function testSearch(int $postCodeId, string $pattern)
{
$client = $this->getClientAuthenticated();
$client->request(
'GET',
"/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
['q' => $pattern]
);
$this->assertResponseIsSuccessful();
}
}

View File

@@ -52,26 +52,6 @@ class AddressToReferenceMatcherControllerTest extends WebTestCase
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_REVIEWED, $address->getRefStatus());
}
/**
* @dataProvider addressUnsyncedProvider
*/
public function testSyncAddressWithReference(int $addressId): void
{
$client = $this->getClientAuthenticated();
$client->request('POST', "/api/1.0/main/address/reference-match/{$addressId}/sync-with-reference");
$this->assertResponseIsSuccessful();
$this->addressRepository = self::getContainer()->get(AddressRepository::class);
$address = $this->addressRepository->find($addressId);
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_MATCH, $address->getRefStatus());
$this->assertEquals($address->getAddressReference()->getStreet(), $address->getStreet());
$this->assertEquals($address->getAddressReference()->getStreetNumber(), $address->getStreetNumber());
$this->assertEquals($address->getAddressReference()->getPoint()->toWKT(), $address->getPoint()->toWKT());
}
public static function addressToReviewProvider(): iterable
{
self::bootKernel();
@@ -98,6 +78,26 @@ class AddressToReferenceMatcherControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider addressUnsyncedProvider
*/
public function testSyncAddressWithReference(int $addressId): void
{
$client = $this->getClientAuthenticated();
$client->request('POST', "/api/1.0/main/address/reference-match/{$addressId}/sync-with-reference");
$this->assertResponseIsSuccessful();
$this->addressRepository = self::getContainer()->get(AddressRepository::class);
$address = $this->addressRepository->find($addressId);
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_MATCH, $address->getRefStatus());
$this->assertEquals($address->getAddressReference()->getStreet(), $address->getStreet());
$this->assertEquals($address->getAddressReference()->getStreetNumber(), $address->getStreetNumber());
$this->assertEquals($address->getAddressReference()->getPoint()->toWKT(), $address->getPoint()->toWKT());
}
public static function addressUnsyncedProvider(): iterable
{
self::bootKernel();

View File

@@ -55,6 +55,25 @@ class NewsItemControllerTest extends WebTestCase
$em->flush();
}
public function testList()
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('News item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(NewsItem $newsItem)
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view");
self::assertResponseIsSuccessful('Single news item admin page loads successfully');
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
@@ -74,23 +93,4 @@ class NewsItemControllerTest extends WebTestCase
yield [$newsItem];
}
public function testList()
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('News item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(NewsItem $newsItem)
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view");
self::assertResponseIsSuccessful('Single news item admin page loads successfully');
}
}

View File

@@ -51,27 +51,6 @@ class NewsItemsHistoryControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$news = new NewsItem();
$news->setContent('test content');
$news->setTitle('Title');
$news->setStartDate(new \DateTimeImmutable('yesterday'));
$em->persist($news);
$em->flush();
static::$toDelete[] = [NewsItem::class, $news];
self::ensureKernelShutdown();
yield [$news->getId()];
}
public function testList()
{
self::ensureKernelShutdown();
@@ -94,4 +73,25 @@ class NewsItemsHistoryControllerTest extends WebTestCase
$this->assertResponseIsSuccessful('test that single news item page loads successfully');
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$news = new NewsItem();
$news->setContent('test content');
$news->setTitle('Title');
$news->setStartDate(new \DateTimeImmutable('yesterday'));
$em->persist($news);
$em->flush();
static::$toDelete[] = [NewsItem::class, $news];
self::ensureKernelShutdown();
yield [$news->getId()];
}
}

View File

@@ -44,33 +44,6 @@ final class NotificationApiControllerTest extends WebTestCase
self::$toDelete = [];
}
public static function generateDataMarkAsRead()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$userRepository = self::getContainer()->get(UserRepository::class);
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
$notification = new Notification();
$notification
->setMessage('Test generated')
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId(0)
->setSender($userB)
->addAddressee($userA)
->setUpdatedAt(new \DateTimeImmutable());
$em->persist($notification);
$em->refresh($notification);
$em->flush();
self::$toDelete[] = [Notification::class, $notification->getId()];
self::ensureKernelShutdown();
yield [$notification->getId()];
}
/**
* @dataProvider generateDataMarkAsRead
*/
@@ -99,4 +72,31 @@ final class NotificationApiControllerTest extends WebTestCase
$this->assertFalse($notification->isReadBy($user));
}
public static function generateDataMarkAsRead()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$userRepository = self::getContainer()->get(UserRepository::class);
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
$notification = new Notification();
$notification
->setMessage('Test generated')
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId(0)
->setSender($userB)
->addAddressee($userA)
->setUpdatedAt(new \DateTimeImmutable());
$em->persist($notification);
$em->refresh($notification);
$em->flush();
self::$toDelete[] = [Notification::class, $notification->getId()];
self::ensureKernelShutdown();
yield [$notification->getId()];
}
}

View File

@@ -24,17 +24,6 @@ final class SearchApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public static function generateSearchData()
{
yield ['per', ['person', 'thirdparty']];
yield ['per', ['thirdparty']];
yield ['per', ['person']];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty']];
}
/**
* @dataProvider generateSearchData
*/
@@ -50,4 +39,15 @@ final class SearchApiControllerTest extends WebTestCase
$this->assertResponseIsSuccessful();
}
public static function generateSearchData()
{
yield ['per', ['person', 'thirdparty']];
yield ['per', ['thirdparty']];
yield ['per', ['person']];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty']];
}
}

View File

@@ -27,25 +27,6 @@ final class UserControllerTest extends WebTestCase
{
use PrepareClientTrait;
public static function dataGenerateUserId()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
/** @var UserPasswordHasherInterface::class $passwordHasher */
$passwordHasher = self::getContainer()->get(UserPasswordHasherInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword($passwordHasher->hashPassword($user, 'password'));
$em->persist($user);
$em->flush();
self::ensureKernelShutdown();
yield [$user->getId(), $user->getUsername()];
}
public function testList()
{
$client = $this->getClientAuthenticatedAsAdmin();
@@ -135,6 +116,25 @@ final class UserControllerTest extends WebTestCase
$this->isPasswordValid($username, $newPassword);
}
public static function dataGenerateUserId()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
/** @var UserPasswordHasherInterface::class $passwordHasher */
$passwordHasher = self::getContainer()->get(UserPasswordHasherInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword($passwordHasher->hashPassword($user, 'password'));
$em->persist($user);
$em->flush();
self::ensureKernelShutdown();
yield [$user->getId(), $user->getUsername()];
}
protected function isPasswordValid($username, $password)
{
/** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher $passwordEncoder */

View File

@@ -31,6 +31,22 @@ final class AgeTest extends KernelTestCase
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
public static function generateQueries(): iterable
{
yield [
@@ -60,20 +76,4 @@ final class AgeTest extends KernelTestCase
],
];
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
}

View File

@@ -31,13 +31,6 @@ final class JsonExtractTest extends KernelTestCase
$this->em = self::getContainer()->get(EntityManagerInterface::class);
}
public static function dataGenerateDql(): iterable
{
yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM '.Country::class.' c', []];
yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM '.Country::class.' c', ['lang' => 'fr']];
}
/**
* @dataProvider dataGenerateDql
*/
@@ -50,4 +43,11 @@ final class JsonExtractTest extends KernelTestCase
$this->assertIsArray($results, 'simply test that the query return a result');
}
public static function dataGenerateDql(): iterable
{
yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM '.Country::class.' c', []];
yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM '.Country::class.' c', ['lang' => 'fr']];
}
}

View File

@@ -44,26 +44,6 @@ final class NotificationTest extends KernelTestCase
$em->flush();
}
public static function generateNotificationData()
{
self::bootKernel();
$userRepository = self::getContainer()->get(UserRepository::class);
$senderId = $userRepository
->findOneBy(['username' => 'center b_social'])
->getId();
$addressesIds = [];
$addressesIds[] = $userRepository
->findOneBy(['username' => 'center b_direction'])
->getId();
yield [
$senderId,
$addressesIds,
];
}
public function testAddAddresseeStoreAnUread()
{
$notification = new Notification();
@@ -139,4 +119,24 @@ final class NotificationTest extends KernelTestCase
$this->assertContains($addresseeId, $unreadIds);
}
}
public static function generateNotificationData()
{
self::bootKernel();
$userRepository = self::getContainer()->get(UserRepository::class);
$senderId = $userRepository
->findOneBy(['username' => 'center b_social'])
->getId();
$addressesIds = [];
$addressesIds[] = $userRepository
->findOneBy(['username' => 'center b_direction'])
->getId();
yield [
$senderId,
$addressesIds,
];
}
}

View File

@@ -93,11 +93,10 @@ class RemoveExpiredExportGenerationCronJobTest extends TestCase
new ExportGeneration('dummy', []),
];
$repo->findExpiredExportGeneration(Argument::that(function ($dateTime) use ($clock) {
$repo->findExpiredExportGeneration(Argument::that(fn ($dateTime) =>
// Ensure the repository is called with the current clock time
return $dateTime instanceof \DateTimeImmutable
&& $dateTime->getTimestamp() === $clock->now()->getTimestamp();
}))->willReturn($expiredExports);
$dateTime instanceof \DateTimeImmutable
&& $dateTime->getTimestamp() === $clock->now()->getTimestamp()))->willReturn($expiredExports);
// Expect one RemoveExportGenerationMessage for each expired export
$bus->dispatch(Argument::that(fn (Envelope $envelope) => $envelope->getMessage() instanceof RemoveExportGenerationMessage))

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