Compare commits

..

271 Commits

Author SHA1 Message Date
cf36070c82 Remove unused @symfony/ux-translator dependency from package.json. 2025-06-18 12:08:22 +02:00
8ee836eec3 Merge remote-tracking branch 'origin/359-fusion-accompanying-period-work' into testing-202505 2025-06-18 12:07:46 +02:00
388030952b Merge remote-tracking branch 'origin/testing-202505' into testing-202505 2025-06-18 12:00:46 +02:00
82a08f1e27 Merge branch '339-partage-d'export-enregistré' into testing-202505
# Conflicts:
#	src/Bundle/ChillPersonBundle/Resources/public/types.ts
2025-06-18 12:00:05 +02:00
925fbaed6d Fix CS 2025-06-18 11:56:20 +02:00
c407c3029f Extend ReferrerFilter to render user descriptions and handle "me" referrers in export data generation. 2025-06-18 11:55:58 +02:00
81b6ae193c Refactor removeUserNotRelatedToJob logic to improve SQL clarity and handle edge cases
We remove the user from the group if the user is deactivated, or of the user does not have any job_history line currently, or if the userjob_id is null in the job_history.
2025-06-18 11:34:32 +02:00
51168ac3c4 In SocialActionFilter, mention the social issue and if a social action is deactivated
All the social action are shown, even the deactivated. So, we append the fact the the social action is also deactivated. We added option in SocialActionRender to achieve this.
2025-06-17 17:45:26 +02:00
12ee091d09 Translate 'Download export' title in wait.html.twig. 2025-06-17 17:10:15 +02:00
49607e431f Set ON DELETE SET NULL for savedExport foreign key in ExportGeneration entity and related migration 2025-06-17 16:20:14 +02:00
ea4cbfe3b9 Update PHPStan configuration to store cache into var directory 2025-06-17 16:19:48 +02:00
9d00b8ae60 Enable filtering statistics by center in Chill configuration: set the default value in config
Set the default value in configuration file. This improves the readability of multiple options.
2025-06-17 16:06:27 +02:00
00350b9efc Refactor scope condition logic in FilterListAccompanyingPeriodHelper
Replaced inline string interpolation with `sprintf` for scope conditions and added user-based condition handling. Introduced new user-specific parameters to enhance query flexibility. Removed unused `AuthorizationHelperForCurrentUserInterface` import.
2025-06-17 16:05:56 +02:00
9a50dad671 Update authorization helper interface in export classes
Replaced `AuthorizationHelperForCurrentUserInterface` with the more generic `AuthorizationHelperInterface` in tests and export helpers. Adjusted method signatures to include the `User` parameter for scope retrieval. Removed unused `centers` mapping in `ListActivity`.
2025-06-17 15:35:04 +02:00
4206d17345 Add debug logging for SQL query execution during export process 2025-06-17 14:25:57 +02:00
110a2e894f Cast exportId to string in logging for consistent logging output 2025-06-17 12:11:11 +02:00
a0daf4428f Merge branch 'master' into 339-partage-d'export-enregistré 2025-06-17 12:05:27 +02:00
f78b8cad9c Cast exportId to string in OnExportGenerationFails logging 2025-06-17 10:39:59 +02:00
6b0c85cdf0 Normalize form data by converting entities using normalizeDoctrineEntity and support array input in denormalizeStringRepresentation. 2025-06-17 10:39:13 +02:00
92b71af239 Merge branch 'master' into 339-partage-d'export-enregistré
# Conflicts:
#	phpstan-baseline-level-4.neon
2025-06-17 09:50:11 +02:00
a0db7cd7e6 Order social actions according to startdate in desc order 2025-06-16 16:27:59 +02:00
011b6a29e4 Remove ux-translator which ended up back into package.json 2025-06-10 11:15:01 +02:00
527dc971d7 Order social actions according to startdate in desc order 2025-06-10 10:23:50 +02:00
43e5bc8337 Merge remote-tracking branch 'origin/339-partage-d'export-enregistré' into testing-202505 2025-05-30 15:00:37 +02:00
845e582c44 Merge remote-tracking branch 'origin/359-fusion-accompanying-period-work' into testing-202505 2025-05-30 14:59:23 +02:00
f3e04bd2bf Merge remote-tracking branch 'origin/master' into testing-202505 2025-05-30 14:58:45 +02:00
96e95dd8f1 Assure checkbox current user is checked in saved export 2025-05-27 16:19:57 +02:00
c40e790425 Add handling and cleanup for expired export generations
Implemented a new cron job to identify and process expired export generations, dispatching messages for their removal. Added corresponding message handler, tests, and configuration updates to handle and orchestrate the deletion workflow.
2025-05-26 17:46:46 +02:00
3a016aa12a Add auto-generated export descriptions and helper service
Introduce `ExportDescriptionHelper` to dynamically generate default descriptions for exports based on current settings. Update controllers, templates, and test cases to support and display the new auto-generated descriptions. This also adds a warning in the UI to prompt users to adjust these descriptions as needed.
2025-05-26 16:44:50 +02:00
be448c650e Refactor SavedExport listing to support filtering.
Introduced filtering capabilities for SavedExport listings by title and description. Moved index functionality to a new `SavedExportIndexController` and updated the repository with the necessary filter logic. Adjusted the Twig template to render the new filter interface.
2025-05-26 14:16:09 +02:00
9f32b5ac48 Refactor BySocialIssueFilter to enhance normalization logic
Added ExportDataNormalizerTrait to streamline data normalization and denormalization processes. Updated constructor to inject dependencies for SocialIssueRepository and NormalizerInterface, improving modularity and maintainability. Adjusted form data normalization methods to utilize the new utilities.
2025-05-26 14:15:51 +02:00
e89f5e4713 Add and enforce 'DUPLICATE' permissions for Saved Exports
Introduce a new 'DUPLICATE' permission in SavedExportVoter and update related logic in the controller and templates to enforce this rule. Ensure only authorized users can duplicate exports and adjust UI elements accordingly for better permission handling.
2025-05-26 12:26:48 +02:00
e79d6d670b Fix CS 2025-05-26 10:35:36 +02:00
9adbde0308 Add export configuration comparison and update options logic
Introduced a method to compare export generation options with saved exports, enabling detection of configuration differences. Updated template logic to conditionally adjust UI elements based on configuration discrepancies. This enhances flexibility when managing saved export options.
2025-05-26 10:35:27 +02:00
fe31cfd544 Refactor export generation to handle saved exports conditionally
Simplified the creation of `ExportGeneration` by introducing a `match` expression to handle cases with and without `savedExport`. This improves readability and ensures consistent handling of saved exports while normalizing configuration options.
2025-05-26 10:28:55 +02:00
e8bca6a502 Do not sync user which are enabled with UserGroup related to UserJob.
Updated SQL queries to include checks for user.enabled status, ensuring proper handling of both enabled and null states. This improves the synchronization logic by aligning it with user activity and account status conditions.
2025-05-23 15:52:11 +02:00
b0918ddd09 Php cs fixes 2025-05-21 09:59:01 +02:00
a368e68abb Merge branch 'testing-202505' of https://gitlab.com/Chill-Projet/chill-bundles into testing-202505 2025-05-21 09:40:32 +02:00
85b9784eef Fix pipeline 2025-05-21 09:40:13 +02:00
26fd16ab07 Transfer evaluations (and related documents) during merge 2025-05-20 16:44:42 +02:00
9a3fef862e Add missing translation accompanying period work duplicate controller 2025-05-20 14:05:02 +02:00
2c01516f71 add changie 2025-05-20 14:00:34 +02:00
246546b313 Retrieve schema to form full tablename and construct sql statements correctly in thirdparty merge service 2025-05-20 12:39:27 +02:00
bfe658d4fd Add pagination to works by period list api endpoint and use fetchResult in frontend 2025-05-08 16:03:52 +02:00
7ea6638c3a remove ux-translator from package.json 2025-04-28 10:51:33 +02:00
b04f0a9aa4 Merge branch '339-partage-d'export-enregistré' into testing-202505 2025-04-25 21:08:38 +02:00
ebdfa04843 Refactor user handling in filters for consistency
Updated `UserWorkingOnCourseFilter` and `ReferrerFilter` to ensure consistent handling of users via `userOrMe` methods. Also adjusted normalization/denormalization to align with these changes, enhancing readability and maintainability.
2025-04-25 21:04:30 +02:00
be213d5b5c Remove unused @symfony/ux-translator dependency
The @symfony/ux-translator package reference was removed from package.json because it is no longer required. This cleanup helps maintain a leaner and more focused dependency list.
2025-04-25 18:54:29 +02:00
4d1032c115 Merge remote-tracking branch 'origin/355-fusion-thirdparty' into testing-202505 2025-04-25 18:31:36 +02:00
e43dfc9a20 Merge remote-tracking branch 'origin/359-fusion-accompanying-period-work' into testing-202505 2025-04-25 18:29:32 +02:00
b11684fb3b set defualt value 2025-04-25 18:26:56 +02:00
b8826c6c0f remove unused property 2025-04-25 18:26:22 +02:00
a996b05ead Fix CS 2025-04-25 18:25:29 +02:00
cfcecf1cdc Refactor export management and configure messenger queue
Refactored the export initialization process by implementing the `ExportManagerAwareInterface` for better consistency. Added configuration to enable handling export requests via the messenger queue for improved task prioritization and execution efficiency.
2025-04-25 18:24:37 +02:00
b6985e0e5f Refactor form data handling in export filters and aggregators
Improved normalization and denormalization of form data by introducing default values and null handling across various filters and aggregators. Added `ExportDataNormalizerTrait` and repository dependencies where necessary to streamline data processing. This ensures more robust data handling and better default value management.
2025-04-25 18:24:26 +02:00
6d76b94644 Refactor title translation logic in SpreadSheetFormatter.
Simplified and streamlined the handling of title translation. Added truncation for titles exceeding 30 characters to ensure proper formatting. This enhances code readability and ensures title length consistency.
2025-04-25 18:23:58 +02:00
ff6ec45575 Add center filtering logic to export generation
Introduced a `filterStatsByCenters` configuration in `ExportGenerator` to enable conditional center filtering during exports. Updated related methods and tests to account for this parameter, ensuring compatibility with both filtered and unfiltered scenarios.
2025-04-25 18:23:37 +02:00
f3fd18e6fb Update export cancel button to redirect to saved exports
Changed the cancel button link in the export generation page to point to the saved exports list. This improves user navigation by directing them to their saved exports instead of the export index page.
2025-04-25 18:23:08 +02:00
c8851a8e8a Handle empty array case in denormalizeDoctrineEntity method
Previously, the method did not explicitly handle empty arrays, which could lead to unexpected behavior. This update ensures that when an empty array is provided as an ID, an empty array is returned immediately.
2025-04-25 17:28:57 +02:00
abdfe49c33 Fix fixedDate logic to handle empty string check
Previously, the `fixedDate` logic did not account for empty strings, which could lead to unexpected behavior. This change ensures that `fixedDate` is validated for both null and empty string values before processing.
2025-04-25 14:38:32 +02:00
e933f3e781 Restrict user fields in UserGroupType based on user jobs.
Refactored the form to conditionally add 'users' and 'adminUsers' fields only if the UserGroup does not have associated user jobs. This ensures data consistency and adjusts the form behavior dynamically based on the entity state.
2025-04-25 13:10:44 +02:00
fb1c34f9c1 Add UserGroup and UserJob synchronization feature
Implement UserGroupRelatedToUserJobSync to manage associations between UserGroups and UserJobs, including creating, updating, and removing relationships. Introduce a cron job to automate the synchronization process and add tests to ensure functionality. Update translations and repository logic as part of the implementation.
2025-04-25 13:07:52 +02:00
d506409d93 Refactor docblocks in UserJob repository interfaces.
Removed redundant PHP docblocks for array return types, as the return type hints already provide sufficient clarity. Added a @template annotation to specify the UserJob entity for the ObjectRepository.
2025-04-25 13:07:41 +02:00
9be8a533ff Add relation between UserGroup and UserJob
Introduce a ManyToOne relationship between UserGroup and UserJob entities to allow synchronization of group members with corresponding UserJobs. This includes a schema migration to add the `userJob_id` column, associated constraints, and an index, as well as updates to the UserGroup entity with new methods for managing the relationship.
2025-04-25 11:44:36 +02:00
b414b27ba9 Make methods private in ExportDataNormalizerTrait
Changed the visibility of several methods from public to private to improve encapsulation and restrict access to internal logic. This enhances code maintainability and prevents unintended use outside the intended scope.
2025-04-25 11:31:31 +02:00
8521672660 Make Security dependency readonly in SavedExportType
Marking the Security property as readonly ensures it cannot be reassigned after initialization. This enforces immutability, improving code safety and clarity.
2025-04-25 11:29:19 +02:00
3f5ce5f841 Refactor filters to support "me" as a user option.
Replaced `PickUserDynamicType` with `PickUserOrMeDynamicType` across filters to enable handling of the "me" option. Introduced normalization and denormalization methods for "me" and adjusted all relevant queries and test cases to accommodate this enhancement.
2025-04-25 11:24:33 +02:00
e1404bf16d Fix test with new signature of FormatterInterface 2025-04-24 22:16:40 +02:00
65b7ed0755 Fix Cs (and problably signature of FormatterInterface) 2025-04-24 22:00:46 +02:00
a74118e5d4 Add context parameter to export generation methods 2025-04-24 22:00:13 +02:00
828739edf5 use context where FormatterInterface is in use 2025-04-24 21:58:18 +02:00
d0811c8118 change FilterInterface::describeAction signature to remove the $format parameter 2025-04-24 21:49:26 +02:00
176bff0551 change FilterInterface::describeAction signature to include context 2025-04-24 21:48:00 +02:00
66c089e862 Refactor title translation logic in SavedExportController
Simplified and centralized title translation by introducing a check for TranslatableInterface. This ensures consistent handling of translatable titles and improves code readability.
2025-04-24 15:44:02 +02:00
aa44577484 Merge branch 'master' into 339-partage-d'export-enregistré
# Conflicts:
#	src/Bundle/ChillMainBundle/Resources/views/Dev/dev.assets.html.twig
2025-04-24 14:26:06 +02:00
8c59cbc6a0 Merge remote-tracking branch 'origin/366-pick-user-or-me' into 339-partage-d'export-enregistré 2025-04-24 14:24:48 +02:00
8c5a7ac3e1 Restrict export filters and aggregators for limited users
Added restrictions on export filters and aggregators based on user permissions. Introduced `ExportConfigProcessor` to handle allowed configurations and updated form components to respect these restrictions. Enhanced validation to enforce access control for unauthorized filter editing.
2025-04-24 14:21:51 +02:00
a6e523ee0a Fix string formatting style in SavedExportController
Replaced double quotes with single quotes for consistency in exception messages and translatable strings. This change improves code readability and maintains uniformity across the file.
2025-04-24 14:21:35 +02:00
d49058805a Refactor aggregator and filter retrieval into a new class
Moved aggregator and filter retrieval logic from ExportGenerator to the newly introduced ExportConfigProcessor class. This improves separation of concerns, simplifies ExportGenerator, and enhances code maintainability and readability. Updated related tests accordingly.
2025-04-24 14:21:16 +02:00
73496e0e1f Add documentation for trait ExportDataNormalizerTrait 2025-04-23 09:46:11 +02:00
973450110b Add duplicate and update options for saved exports
Introduce functionality to duplicate saved exports and update options directly from export generations. Update translations, controllers, views, and entities to support the new features, providing better flexibility and user experience around saved export management.
2025-04-23 09:11:50 +02:00
0f6b10aa0a Refactor SavedExport permissions and voter logic
Revised SavedExportVoter to improve consistency and streamline permission checks. Updated tests, controller logic, and templates to align with new voter structure and attributes. Fixed typos in permission constants and added checks for delete/edit actions in the UI.
2025-04-18 14:09:13 +02:00
edeb8edbea Add role-based access controls for export functionality
Introduced `CHILL_MAIN_COMPOSE_EXPORT` and `CHILL_MAIN_GENERATE_SAVED_EXPORT` roles for managing export creation and execution permissions. Updated access checks, menu routing, and templates to align with the new roles. Added a migration to extend existing permission groups with the `CHILL_MAIN_COMPOSE_EXPORT` role.
2025-04-17 17:34:09 +02:00
fc8e3789e0 Refactor SavedExportVoter to improve export permission check
Revised the permission logic in `canUserGenerate` to enhance clarity and maintainability. Replaced nested condition with early return and updated the export permission check to use `isGrantedForElement`.
2025-04-17 15:47:38 +02:00
52a80f9621 Add flash message for successful ACPW merge
This update informs users when accompanying period works are successfully merged by adding a success flash message. A new translatable message was also added for proper localization of this notification.
2025-04-15 14:54:15 +02:00
5632697c05 Fixes 2025-04-15 14:46:00 +02:00
75932b0e29 Fixes 2025-04-15 14:08:39 +02:00
7e2bf91e09 Fixes 2025-04-15 14:02:25 +02:00
0581b59dbd Merge remote-tracking branch 'refs/remotes/origin/master' into 359-fusion-accompanying-period-work 2025-04-14 15:53:55 +02:00
4661ba9932 Fix translation key casing for 'First name' label
Updated the translation key from 'firstName' to 'First name' in the details view template to ensure consistency with the expected translation format. This improves clarity and alignment with other labels.
2025-04-14 15:51:44 +02:00
f85b0c8cf7 Add flash message for successful third-party merge
This commit introduces a flash message to inform users when a third-party merge is successful. It uses Symfony's `FlashBag` and `TranslatableMessage` for better user feedback and localization support.
2025-04-14 15:51:43 +02:00
e701e96187 Fix merging: update the thirdparty related column and not the other one 2025-04-14 15:49:51 +02:00
d9d151aa89 Merge branch 'refs/heads/master' into 355-fusion-thirdparty 2025-04-14 11:29:46 +02:00
a14ed78e25 [wip] temporary changie 2025-04-14 11:01:03 +02:00
420dd4f868 Add SHARE permission to SavedExportVoter with tests
Introduced the SHARE attribute and updated SavedExportVoter to handle it. Added new functionality to check if a SavedExport is shared with a specific user and included corresponding unit tests for both the voter and entity behaviors.
2025-04-14 10:59:47 +02:00
3d9b9ea672 Layout of saved export page 2025-04-14 10:59:31 +02:00
9f12b42961 saved export: add form to share the export 2025-04-14 10:59:09 +02:00
2842548c17 Layout for export list 2025-04-14 10:58:52 +02:00
35f5501489 Add sharing functionality for SavedExport with users/groups
Introduced support for sharing SavedExport entities with individual users or groups. Added new collections, methods for adding/removing/viewing shares, and a migration to create relevant join tables in the database. This enhances collaboration by enabling flexible access control.
2025-04-14 10:58:13 +02:00
5e2d960a19 Fix comparison in new GenerateButton 2025-04-11 09:34:21 +02:00
4129283a58 fix tests 2025-04-08 17:41:34 +02:00
f807d62dab Merge branch 'refs/heads/master' into 339-partage-d'export-enregistré 2025-04-08 17:30:38 +02:00
5a7bba83f7 Ignore PHPStan warning for deprecated method usage
Added a PHPStan ignore comment to bypass warnings for calling a deprecated method, which remains necessary for compatibility. This ensures functionality while avoiding static analysis issues.
2025-04-08 17:30:08 +02:00
50c75dff1a Refactor export method to use FormattedExportGeneration.
Replaces direct BinaryFileResponse usage with FormattedExportGeneration for handling file exports. Removes unnecessary imports and ensures temporary file cleanup after processing.
2025-04-08 17:29:10 +02:00
e37f3e7c37 Refactor GenderFilter to enhance data normalization.
Introduced logic to map and transform gender data for normalization and denormalization, aligning with expected formats. Updated associated tests to cover new scenarios and ensure consistent behavior.
2025-04-08 17:24:38 +02:00
b6375cad6c fix tests 2025-04-08 16:32:56 +02:00
8516c87a14 fix tests 2025-04-08 15:50:03 +02:00
8c5abbff74 Update type handling in entity normalization methods
Extended support for `string` types in `normalizeDoctrineEntity` and `denormalizeDoctrineEntity` methods. This ensures compatibility with a broader range of identifier formats and improves flexibility in entity processing.
2025-04-08 15:49:55 +02:00
e0f94ae900 Handle null input in fromNormalized method
Updated the `fromNormalized` method to return null when provided with a null input. This ensures better handling of edge cases and prevents potential errors from null data.
2025-04-08 15:49:04 +02:00
f683e45b6e Fix tests 2025-04-08 15:42:03 +02:00
b1b7fb6401 Fix tests 2025-04-08 15:40:33 +02:00
8fa06e143d Fix tests 2025-04-08 15:37:27 +02:00
ec3d901d2f Fix tests 2025-04-08 15:30:25 +02:00
f12d689382 Fix tests 2025-04-08 15:24:52 +02:00
1c04219859 Add RegroupmentRepository dependency to ExportConfigNormalizer
This change introduces the RegroupmentRepositoryInterface as a new dependency in the ExportConfigNormalizer. It updates related test cases to mock and pass the new repository, ensuring proper coverage and functionality.
2025-04-08 15:24:40 +02:00
2b88593e64 Add RegroupmentRepositoryInterface and integrate it
Created a new `RegroupmentRepositoryInterface` to define repository methods for Regroupment entities. Updated `RegroupmentRepository` to implement this interface, and replaced its usage in `ExportConfigNormalizer` for better abstraction and testability.
2025-04-08 15:24:18 +02:00
ee65c46d2a Add handling for iterable values in test assertions
This update ensures that iterable values are properly handled in test assertions by skipping over them when needed. The change was applied consistently across AbstractAggregatorTest, AbstractFilterTest, and AbstractExportTest to improve test robustness.
2025-04-08 14:57:37 +02:00
7c239eaf6a Refactor entity check logic in test assertions.
Replaced the usage of `$em->contains` with a more generic `is_object` and `method_exists` check for `getId`. This improves test flexibility and removes reliance on the EntityManager in these cases.
2025-04-08 14:42:40 +02:00
694b1f3c1f Add data normalization test and context handling improvements
Introduced data normalization testing methods to validate form data processing. Enhanced `initiateQuery` to include `ExportGenerationContext` with user retrieval logic for improved query handling. These changes strengthen data integrity and contextual query execution within export functionality.
2025-04-08 14:42:34 +02:00
80b9ce3c3e Refactor tests to include ExportGenerationContext and data normalization
Added ExportGenerationContext usage in aggregator and filter tests to provide user context during query alterations. Introduced new data normalization tests and related logic, ensuring consistency and validation for normalized and denormalized forms. Refactored `getUser` methods for better scoping and reusability.
2025-04-08 14:26:19 +02:00
3f218e7183 Add data provider and test for form data normalization
Introduced a `dataProviderFormDataToNormalize` method to supply test cases for normalization. Added `testDataNormalization` to validate form data normalization and ensure consistency between normalized and denormalized data.
2025-04-08 14:09:26 +02:00
a2713041da fix cs 2025-04-08 14:09:16 +02:00
3a904e8ea1 Fix method return type from closure to callable
Updated the return type of the `getLabels` method to `callable` for better type accuracy. This aligns with PHP standards and improves code clarity and maintainability.
2025-04-08 13:40:11 +02:00
6f1a26742d Refactor method signatures for stricter type safety
Updated method parameters to enforce stricter type declarations, improving code clarity and reducing potential runtime errors. This change ensures better consistency across interfaces and aligns with modern PHP typing practices.
2025-04-08 13:40:05 +02:00
0d0a626f50 Implements return types on aggregators 2025-04-08 12:38:52 +02:00
ec5f4ed1d6 Refactor type annotations in export interfaces
Updated return type annotations in `AggregatorInterface` for clarity and adjusted generics in `ListInterface` to better reflect supported query types. This improves code readability and strengthens type safety.
2025-04-08 12:38:38 +02:00
d9251239f7 Remove unused CSVFormatter and improve translations handling
CSVFormatter was deprecated and is no longer in use, so it was removed. Additionally, translation handling was enhanced across multiple formatters, supporting `TranslatableInterface` to ensure better localization compatibility.
2025-04-08 10:38:07 +02:00
b2d3d806b6 Fix PHPDocs and return type declarations in export interfaces
Corrected typos in PHPDocs, added missing type templates, and ensured proper return type declarations. These changes improve code readability, consistency, and type safety across export-related interfaces.
2025-04-08 10:37:47 +02:00
8e952cc966 Implements new return types 2025-04-07 14:35:29 +02:00
1c4ee37507 Refactor entity normalization to handle Doctrine Collection.
Updated `normalizeDoctrineEntity` to support normalizing `Collection` objects and handle null values in arrays. This ensures compatibility with a broader range of entity structures and improves data integrity during normalization.
2025-04-07 14:35:07 +02:00
8331a836f2 Add explicit return types and TranslatableInterface support
Updated interfaces to include explicit return types for improved type safety and readability. Integrated support for Symfony's TranslatableInterface in relevant methods to enhance translation handling.
2025-04-07 14:26:11 +02:00
2482dcc62e Refactor ExportManager integration and remove ExportsCompilerPass
Replaced direct ExportManager dependencies in formatters with an ExportManagerAwareTrait to decouple components and enhance flexibility. Removed the ExportsCompilerPass as it is no longer required, and adjusted service configurations to improve reliability and consistency.
2025-04-07 12:42:28 +02:00
f9a55a1bfd Fix debug artifact in ExportFormHelper
Removed unnecessary `dump()` calls when resolving centers and regroupments. This ensures cleaner processing and
2025-04-07 12:37:29 +02:00
15aa565caf Add custom exception classes for export errors
Introduced `ExportRuntimeException` and `ExportLogicException` to better categorize exceptions in the export process. Updated `ExportGenerationException` to extend `ExportRuntimeException` for clearer hierarchy and improved maintainability.
2025-04-07 11:38:42 +02:00
566b72ec9e Add $context parameter to export query generation
This update introduces the $context parameter to the query generator method. It ensures additional contextual data can be passed and utilized, improving the flexibility of export generation logic.
2025-04-07 11:31:52 +02:00
bb42ee25ff Update initiateQuery method to include ExportGenerationContext
The `initiateQuery` method now consistently incorporates the `ExportGenerationContext` parameter across various export classes, improving functionality and standardization. This change ensures compatibility and alignment with the expected method signature.
2025-04-07 11:29:04 +02:00
aebeca1d7a Use readonly properties in constructors for better immutability.
Converted constructor properties to readonly where applicable, ensuring immutability and promoting safer code practices. Updated throwable class reference for clarity and consistency.
2025-04-05 00:56:54 +02:00
1955249a60 Fix type handling in JSON decode for migration script
Ensure `json_decode` explicitly casts database values to strings to avoid type errors during migration. This prevents potential issues when processing `options` fields that may not already be of string type.
2025-04-05 00:53:01 +02:00
3b0a4e9c73 Refactor exports to include ExportGenerationContext.
Updated initiateQuery methods to pass ExportGenerationContext, enabling better user-specific query filtering. Removed redundant user retrieval in FilterListAccompanyingPeriodHelper, now relying on context-provided user data.
2025-04-05 00:35:42 +02:00
b5fd9cf4af Add SavedExportOptionsMigrator for migrating export options
Introduced SavedExportOptionsMigrator to handle the migration of saved export options with structures ensuring versioning. Added unit tests to validate transformation logic and edge cases for better reliability.
2025-04-05 00:35:16 +02:00
bb30ddc876 Add logging and validation to export generation handler
Introduce a logger to track processing steps, durations, and outcomes in `ExportRequestGenerationMessageHandler`. Add validation to prevent reprocessing of already generated export objects, ensuring robust error handling and improved traceability.
2025-04-05 00:08:37 +02:00
5ebb53173e Fix PHPStan comment format in ExportGenerator.php
Updated the PHPStan ignore comment to use a single asterisk for consistency with comment style. This change does not affect functionality but improves code readability and adherence to coding standards.
2025-04-05 00:08:21 +02:00
d1d6a00ebf Add failure handling for export generation errors
Introduce `OnExportGenerationFails` subscriber to handle export generation failures. It logs errors, updates the export status to failure, and records generation errors. Added tests to validate the new functionality.
2025-04-05 00:07:08 +02:00
e48bec490c Compute allowed centers and regroupment at the time of generating the export 2025-04-03 17:47:46 +02:00
128d365a72 Convert Rolling date using the clock instead of the built-in pivot date 2025-04-03 17:47:08 +02:00
10f66afdcd Update date formatting and add null-safe checks for pivot dates
Revised the date normalization format to align with ISO standards and ensure consistency. Additionally, introduced null-safe checks for pivot dates to avoid potential errors when these values are unset. Updated related tests to reflect these changes.
2025-04-02 11:35:47 +02:00
89aed74355 Refactor RollingDateConverter to use ClockInterface
Introduced ClockInterface for better time mocking and handling, replacing default instantiation with dependency injection. Adjusted RollingDate logic to accept nullable pivot dates and updated related tests for improved flexibility and accuracy.
2025-04-02 11:08:01 +02:00
b3bf405c5b Fix merge service and test 2025-04-01 16:15:34 +02:00
9124fa68e8 Refactor flash message handling to use translatable messages
Replaced raw translations with Symfony's TranslatableMessage for flash messages in the controller. Updated Twig templates to use the `|trans` filter for consistency in rendering translations. This ensures better handling of multilingual content across the application.
2025-04-01 14:47:39 +02:00
68b61b7d8a Refactor export-related controllers and helpers
Removed unused dependencies, obsolete methods, and redundant parameters across ExportController, ExportFormHelper, and SavedExportController. Simplified session handling and aligned method signatures to improve maintainability and code readability.
2025-04-01 14:19:15 +02:00
80d8f967fa Moved export listing functionality from ExportController to a new ExportIndexController
for improved separation of concerns. Updated tests accordingly to reflect the new controller structure.
2025-04-01 13:20:48 +02:00
1375a41de2 Refactor export handling to support ExportGeneration and SavedExport
Unified handling of ExportGeneration and SavedExport entities by introducing the SavedExportOrExportGenerationRepository. Simplified form data conversion using ExportConfigNormalizer, removing redundant FormBuilder logic. Adjusted dependent methods and controllers to accommodate these changes.
2025-04-01 10:38:51 +02:00
4fa4d3b65c Phpstan and cs fixes 2025-03-27 14:32:06 +01:00
bd4c34cc1d Fix eslint issues and add ts interfaces for typing 2025-03-27 14:26:43 +01:00
4cea678e93 Fix updating of manyToMany relationships 2025-03-27 13:34:16 +01:00
5e6833975b Fix handling comments and workflows on acpw 2025-03-26 20:25:54 +01:00
f523b9adb3 Fix typing errors 2025-03-26 20:25:39 +01:00
a211549432 Adjust template and add translations 2025-03-26 15:16:27 +01:00
17b1363113 Fixes after rebase + apply item styling for accompanying course work 2025-03-26 14:08:45 +01:00
3356ed8e57 Correct for loop to display accompanying period list items 2025-03-24 16:13:56 +01:00
2a7fa517ee Only show merge button if there are more than 1 works attached to the parcours 2025-03-24 16:07:47 +01:00
95972399a1 Remove redundant lines in test 2025-03-24 15:28:58 +01:00
85781c8e14 Use item renderbox for display of accompanyingperiodwork 2025-03-19 11:04:01 +01:00
00eb435896 Add chevron icon in merge button 2025-03-19 11:04:01 +01:00
ed71cffd6a Change behavior of information exchange between backend and frontend 2025-03-19 11:03:59 +01:00
ae679e6997 Fix merge service and passing of json to vue 2025-03-19 11:03:53 +01:00
e1d308fd97 WIP create new picker for accompanying period works 2025-03-19 11:03:42 +01:00
d9acda67e3 WIP dynamic picking of accompanying period work 2025-03-19 11:03:42 +01:00
e88da74882 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:03:41 +01:00
591c44d1a0 Create types 2025-03-19 11:03:18 +01:00
bf04b7981c Improve merge service according to specifications 2025-03-19 11:03:02 +01:00
df33eec30f WIP merge service 2025-03-19 11:03:00 +01:00
c657c98918 Styling and organization of components 2025-03-19 11:02:55 +01:00
ef5eb5b907 Open modal to select acpw 2025-03-19 11:02:27 +01:00
d683fe002d Different approach to creating acpw selector 2025-03-19 11:02:25 +01:00
555bbca59b WIP create new picker for accompanying period works 2025-03-19 11:02:22 +01:00
e9e9d5c458 WIP dynamic picking of accompanying period work 2025-03-19 11:02:22 +01:00
b1842a33ae WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:02:19 +01:00
6afeaccf24 Improve merge service according to specifications 2025-03-19 11:01:52 +01:00
fb76bac480 WIP merge service 2025-03-19 11:01:49 +01:00
6ded185289 Treat duplicate in backend and setup confirm page of merge 2025-03-19 11:00:40 +01:00
95adc29f9d WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
4d0c3e683f WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
018aafc773 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
c4aea4efc2 Create types 2025-03-19 11:00:40 +01:00
225e3ca13f Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
8c1fa7956a WIP merge service 2025-03-19 11:00:40 +01:00
e253d1b276 Styling and organization of components 2025-03-19 11:00:40 +01:00
a52aac2d98 Update package.json 2025-03-19 11:00:40 +01:00
9e8cf60dd8 Open modal to select acpw 2025-03-19 11:00:40 +01:00
7682d81d50 Different approach to creating acpw selector 2025-03-19 11:00:40 +01:00
5d31ce96c1 WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
81ef64a246 WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
49d1f78001 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
0d0f3528e2 Add (temporary) types in Main and ThirdpartyBundle 2025-03-19 11:00:40 +01:00
d97d5e689a Create types 2025-03-19 11:00:40 +01:00
95d80ce13e Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
668720984d WIP merge service 2025-03-19 11:00:40 +01:00
245c3fa121 First commit - changie for feature 2025-03-19 11:00:40 +01:00
1af3e4c7ec Eslint fixes and phpcs fixer 2025-03-18 14:39:19 +01:00
5d0fc7a189 remove comment 2025-03-18 14:25:59 +01:00
483d50e776 Return a string 'me' in EntityToJsonTransformer.php 2025-03-18 14:25:59 +01:00
b4c6ccf309 Add missing rendering condition 2025-03-18 14:25:59 +01:00
ac6a81cbd8 Adjust styling of current user selected 2025-03-18 14:25:59 +01:00
9503cb89b6 Setup if condition in EntityToJsonTransformer.php for when 'me' is passed 2025-03-18 14:25:59 +01:00
b130dbdcdc Add translations for current user in pick entity component 2025-03-18 14:25:59 +01:00
b2b1865837 WIP backend userOrMePicker 2025-03-18 14:25:59 +01:00
ec5c4d51b3 Frontend: display and behavior of pickUserOrMe form field 2025-03-18 14:25:56 +01:00
180437f637 Add generation button for saved exports and improve UI flow
Introduce a Vue.js component to handle the generation of saved exports directly via a new button. Adjust related API endpoints, improve status handling, and remove redundant backend logic. Minor UI enhancements and translation updates included.
2025-03-17 14:25:57 +01:00
0c2508d26d Refactor ExportGeneration to improve serialization and usage
Streamlined the `ExportGeneration` entity by adding a `getStatus` accessor and updating the serialization group. Refactored frontend logic to use computed properties for `status` and `storedObject`. Simplified API methods to work with the updated `ExportGeneration` interface for consistent handling.
2025-03-17 10:41:54 +01:00
b2d8d21f04 Add export generation creation from saved export endpoint
Introduce a new controller for creating export generations from saved exports using a POST endpoint. Updates include a new route, serialization groups, and a corresponding test to validate functionality.
2025-03-16 22:47:57 +01:00
6a2aa77ecc Add setter for SavedExport and update SavedExport logic
Introduced a `setSavedExport` method in `ExportGeneration` entity to enable better association handling. Updated `SavedExportController` to use this setter and adjusted template reference for the 'new' action. Removed unused `TranslatableStringHelperInterface` dependency for cleaner code.
2025-03-14 17:26:40 +01:00
e7cd9e00f9 Add functionality to save exports from export generations
Introduced a new route and controller method to create saved exports directly from an export generation. Updated the Twig template to include a "Save" button, enabling users to utilize this new feature seamlessly. This enhances export management and provides a more convenient user experience.
2025-03-13 18:05:56 +01:00
39f60b5b34 Add association between ExportGeneration and SavedExport
This update introduces a relationship between ExportGeneration and SavedExport. It includes a new SavedExport field in the ExportGeneration entity and the corresponding database migration. These changes enable ExportGeneration to reference its originating SavedExport if applicable.
2025-03-13 18:05:34 +01:00
c9c29b9105 Add ExportGenerationVoter and integrate it into StoredObjectVoter
Introduced ExportGenerationVoter to handle specific view permissions for ExportGeneration entities. Updated ExportGenerationStoredObjectVoter to delegate permission checks to the new voter using Symfony's security system. This improves separation of concerns and reusability of authorization logic.
2025-03-13 17:53:10 +01:00
fb806a9579 Add title and creation date to export data handling
Enhanced export functionality by including `title` and `createdDate` in data passed to the Vue app. Updated controllers, templates, and components to handle and display the new fields, improving export file naming and user interface. Removed a debug `dump` call for cleaner code.
2025-03-13 17:36:47 +01:00
70ca4acafb Add voter for ExportGeneration stored object authorization
Introduces `ExportGenerationStoredObjectVoter` to handle permissions for stored objects linked to export generations. Implements entity association retrieval in `ExportGenerationRepository` by adhering to `AssociatedEntityToStoredObjectInterface`.
2025-03-13 17:23:05 +01:00
bd61eedfbb Migrate export generation flow from plain JS to Vue
Replaced the old JavaScript-based export generation logic with a Vue.js implementation to improve maintainability and modularity. Introduced a new API endpoint to fetch export status, updated the Webpack config, and integrated translations and Twig templates for the new flow. The Vue-based solution enhances user feedback and error handling during the export process.
2025-03-13 17:03:14 +01:00
80ce7f0bf1 Implement ExportGenerationController with waiting and status routes
Added a new controller to handle export generation wait views and status responses. Updated ExportController to redirect to the 'wait' route after export generation. Introduced tests to validate object status handling for pending and ready states.
2025-03-13 15:29:49 +01:00
1ebf838bde Refactor ExportRequestGenerationMessageHandler logic.
Enhance the message handler to properly manage stored objects by leveraging `StoredObjectManagerInterface` and `EntityManagerInterface`. Added logic for writing generated content, updating stored object status, and ensuring data persistence with a DB flush.
2025-03-13 14:19:00 +01:00
85a9c6bb67 Refactor export manager and normalize configuration handling
Simplified formatter and aggregator logic by removing redundant fields and improving method checks (e.g., `hasAggregator`, `hasFilter`). Adjusted test cases to align with the updated structure and added tests for empty data normalization and denormalization. Improved code readability and ensured better handling of edge cases in export data processing.
2025-03-13 14:18:54 +01:00
db073fc920 Refactor ExportController to handle async generation of exports 2025-03-13 14:18:23 +01:00
46ebfca28f add ExportNormalizerConfig service to DI configuration file 2025-03-12 22:15:43 +01:00
22a2605381 fix messenger config: remove AuthenticationMiddleware 2025-03-12 22:15:25 +01:00
4f6a7116a4 fixup! Add ChillBundle normalization methods rector rule 2025-03-12 21:57:54 +01:00
cac7d33a44 fixup! Add ChillBundle normalization methods rector rule 2025-03-12 21:51:24 +01:00
0609e3f4c3 fixup! Add Rector Rule to inject normalization methods into export classes 2025-03-12 21:50:33 +01:00
3a738179f2 use correct repository for filters and aggregators 2025-03-12 17:41:59 +01:00
a789bf5e1c Add return type for getTargets() method in validator
Ensure the `getTargets()` method specifies its return type for better code clarity and type safety. This update aligns with modern PHP practices and improves maintainability.
2025-03-12 17:41:28 +01:00
0a21fada42 fixup! Add normalization and denormalization for RollingDate 2025-03-12 10:04:12 +01:00
4f030eb11a Add date normalization and denormalization functionality
Introduce methods to normalize and denormalize DateTime objects using specific formats. Also, implement corresponding unit tests to ensure correct handling of single and multiple Doctrine entities, as well as date normalization.
2025-03-12 10:00:39 +01:00
0d2a487ae7 Add new normalization methods on filters, aggregators exports and formatter by applying rector rule 2025-03-11 22:28:42 +01:00
6cb23344fc Add ChillBundle normalization methods rector rule
This commit introduces a new rector rule to add normalization methods for ChillBundle exports. It removes an outdated rector configuration and ensures alignment with current standards.
2025-03-11 22:18:04 +01:00
da0d7a3b9e fixup! Add Rector Rule to inject normalization methods into export classes 2025-03-11 22:17:42 +01:00
fcc61aa4c0 fixup! Add Rector Rule to inject normalization methods into export classes 2025-03-11 16:21:16 +01:00
21ec96b75c fixup! Add Rector Rule to inject normalization methods into export classes 2025-03-11 15:31:30 +01:00
93f934152f !fixup fix rector rule 2025-03-11 15:28:28 +01:00
49f4cce72a fixup! Rename getVersion to getNormalizationVersion. 2025-03-11 15:09:26 +01:00
e69b679938 Add Rector Rule to inject normalization methods into export classes
This update introduces a Rector rule to automatically add `normalizeFormData`, `denormalizeFormData`, and `getNormalizationVersion` methods to export-related classes implementing specific interfaces. It ensures consistency and reduces manual work by leveraging traits and default implementations for normalizing form data. Test fixtures and configurations are included to validate and support this functionality.
2025-03-11 15:08:48 +01:00
229f9b7125 Rename getVersion to getNormalizationVersion.
This change improves clarity by making the method name more descriptive and aligned with its purpose. All relevant interfaces, classes, and tests have been updated accordingly to reflect this renaming.
2025-03-10 21:37:32 +01:00
4fc433cd63 Rename and implement form data normalization for filters
The `StepFilterTest` was renamed to `StepFilterOnDateTest` for improved clarity. Added `normalizeFormData` and `denormalizeFormData` methods to `UserJobFilter` and `StepFilterOnDate` for handling form data conversions. Also introduced `ExportDataNormalizerTrait` to enhance consistency in data normalization processes.
2025-03-10 21:17:25 +01:00
2c91d2e10f Add normalization and denormalization for RollingDate
Introduce methods to normalize and recreate RollingDate objects, ensuring consistent serialization and deserialization. Includes corresponding tests for both cases with and without a pivot date.
2025-03-10 21:07:54 +01:00
4302506471 Fix translation and cs-fixes 2025-03-05 19:43:16 +01:00
09b7558e92 Remove the transferData method from mergeService 2025-03-05 19:27:32 +01:00
6ba5c91ee6 redo test 2025-03-05 19:27:32 +01:00
535409e529 phpstan and php cs fixer 2025-03-05 19:27:32 +01:00
9979378e78 validate thirdparty merge 2025-03-05 19:27:32 +01:00
58291c7402 Add suggested thirdparties, with same parent, for thirdparties of kind 'child' 2025-03-05 19:27:32 +01:00
3d397c0145 Adjust templates and add translations 2025-03-05 19:27:32 +01:00
d84f3ee5ad Modify translations and if conditions in updateReference method 2025-03-05 19:27:32 +01:00
6db16e6d0b Add translations 2025-03-05 19:27:30 +01:00
ed60c6aaa3 Remove dumps and inject thirdparty directly into controller action 2025-03-05 19:27:11 +01:00
13b1d20ade Resolve manyToMany cases 2025-03-05 19:27:11 +01:00
5999c73c98 Refactor merge 2025-03-05 19:27:10 +01:00
580366de6d Continue thirdparty merge controller and create views 2025-03-05 19:27:03 +01:00
7f69f21b64 Renamen and reorganize thirdparty merge files 2025-03-05 19:26:50 +01:00
683a0bc4e9 Create thirdparty merge test 2025-03-05 19:26:06 +01:00
4d6d40629f Create thirdparty merge manager 2025-03-05 19:26:06 +01:00
2185791665 Use sql statements for transferData method and fix updateReferences method 2025-03-05 19:26:06 +01:00
bf14c92567 Continue thirdparty merge controller and create views 2025-03-05 19:26:04 +01:00
7c1c1ed800 Renamen and reorganize thirdparty merge files 2025-03-05 19:25:31 +01:00
3b3659f13f WIP Create thirdparty merge controller 2025-03-05 19:25:31 +01:00
ebfdc57fcf Create thirdparty merge test 2025-03-05 19:25:31 +01:00
a562690512 Create thirdparty merge manager 2025-03-05 19:25:31 +01:00
1751c65731 add missing arguments to method alterQuery (wip) 2025-02-24 16:49:15 +01:00
791f5bb4be Remove custom Messenger authentication logic (WIP)
Eliminated the `AuthenticationMiddleware`, `AuthenticatedMessengerToken`, and `AuthenticationStamp` classes, along with their service declarations. This cleanup removes unused or unnecessary code, simplifying the project and reducing maintenance overhead.
2025-02-24 16:13:20 +01:00
2c812fc5fe Generate export using denormalization 2025-02-23 23:16:30 +01:00
1f1d38acef infrastructure for normalizing form config [WIP] 2025-02-21 15:08:09 +01:00
057c34610d First step to async generation [WIP] 2025-02-20 14:33:50 +01:00
732b7dc8f7 Add missing return types
This will avoid deprecation messages
2025-02-19 21:40:39 +01:00
cb068cdee7 Add ExportGeneration entity and migration
Introduce the `ExportGeneration` entity for managing export generation data, including related properties such as `exportAlias`, `options`, and `deleteAt`. Add corresponding database migration to create the `chill_main_export_generation` table with necessary constraints and indices. Update `composer.json` to include the `symfony/uid` package.
2025-02-19 16:45:37 +01:00
244 changed files with 4870 additions and 5887 deletions

View File

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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,6 +0,0 @@
kind: Feature
body: Show filters on list pages unfolded by default
time: 2025-07-22T15:50:39.338057044+02:00
custom:
Issue: "399"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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

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

View File

@@ -0,0 +1,6 @@
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: Fixed
body: adjust display logic for accompanying period dates, include closing date if period is closed.
time: 2025-08-06T13:46:09.241584292+02:00
custom:
Issue: "382"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: add min and step attributes to integer field in DateIntervalType
time: 2025-08-06T17:35:27.413787704+02:00
custom:
Issue: "384"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
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

View File

@@ -1,6 +0,0 @@
kind: UX
body: Limit display of participations in event list
time: 2025-07-22T13:26:37.500656935+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,22 +0,0 @@
## 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

View File

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

View File

@@ -1,74 +0,0 @@
## 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

@@ -1,4 +0,0 @@
## v4.0.1 - 2025-07-08
### Fixed
* Fix package.json for compilation

View File

@@ -1,4 +0,0 @@
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork

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 - **Backend**: PHP 8.3+, Symfony 5.4
- **Frontend**: JavaScript/TypeScript, Vue.js 3, Bootstrap 5 - **Frontend**: JavaScript/TypeScript, Vue.js 3, Bootstrap 5
- **Build Tools**: Webpack Encore, Yarn - **Build Tools**: Webpack Encore, Yarn
- **Database**: PostgreSQL with materialized views. We do not support other databases. - **Database**: PostgreSQL with materialized views
- **Other Services**: Redis, AMQP (RabbitMQ), SMTP - **Other Services**: Redis, AMQP (RabbitMQ), SMTP
## Project Structure ## Project Structure
@@ -149,93 +149,14 @@ Key configuration files:
- `package.json`: JavaScript dependencies and scripts - `package.json`: JavaScript dependencies and scripts
- `.env`: Default environment variables. Must usually not be updated: use `.env.local` instead. - `.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.
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
where we have full and easy control of the date.
### Testing Information ### 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. 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.
#### Use of mock in tests
##### General mocking
For creating mock, we prefer using prophecy (library phpspec/prophecy). For creating mock, we prefer using prophecy (library phpspec/prophecy).
##### Useful helpers and tips that avoid create a mock
Some notable implementations that are tests helper, and avoid to create a mock:
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
```php
use Symfony\Component\Mailer\Transport\InMemoryTransport;
use \Symfony\Component\Mailer\Mailer;
$transport = new InMemoryTransport();
$mailer = new Mailer($transport);
// After sending:
$messages = $transport->getSent(); // array of SentMessage
```
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
##### When we prefer not creating a mock
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
##### Mocking final and readonly classes
Classes marked as final can't be mocked. To avoid that, either:
- we remove the `final` keyword from the class;
- we extract an interface from the final class.
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
#### Running Tests #### Running Tests
The tests are run from the project's root (not from the bundle's root).
```bash ```bash
# Run all tests # Run all tests
vendor/bin/phpunit vendor/bin/phpunit
@@ -297,7 +218,7 @@ class TicketTest extends TestCase
#### Test Database #### Test Database
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. 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.
### Code Quality Tools ### Code Quality Tools

View File

@@ -6,138 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
## v4.0.1 - 2025-07-08
### Fixed
* Fix package.json for compilation
## 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 ## v3.10.3 - 2025-03-18
### DX ### DX
* Eslint fixes * Eslint fixes

View File

@@ -62,10 +62,8 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority 'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async 'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async 'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
# end of routes added by chill-bundles recipes # end of routes added by chill-bundles recipes
# Route your messages to the transports # Route your messages to the transports
# 'App\Message\YourMessage': async # 'App\Message\YourMessage': async

View File

@@ -2154,6 +2154,11 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php 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\\.$#" message: "#^PHPDoc tag @var for property Chill\\\\MainBundle\\\\Export\\\\Helper\\\\ExportAddressHelper\\:\\:\\$unitNamesKeysCache contains unresolvable type\\.$#"
count: 1 count: 1

View File

@@ -37,6 +37,9 @@ return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class); $rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::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 // part of the symfony 54 rules
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); $rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
$rectorConfig->rule(\Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class); $rectorConfig->rule(\Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class);

View File

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

View File

@@ -17,6 +17,7 @@ use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* ActivityReason controller. * ActivityReason controller.
@@ -49,6 +50,28 @@ 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. * Lists all ActivityReason entities.
*/ */
@@ -79,10 +102,29 @@ 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. * Edits an existing ActivityReason entity.
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update')] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update', methods: ['POST', 'PUT'])]
public function updateAction(Request $request, mixed $id) public function updateAction(Request $request, mixed $id)
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -138,7 +180,7 @@ class ActivityReasonController extends AbstractController
{ {
$form = $this->createForm(ActivityReasonType::class, $entity, [ $form = $this->createForm(ActivityReasonType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]), 'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]),
'method' => 'POST', 'method' => 'PUT',
]); ]);
$form->add('submit', SubmitType::class, ['label' => 'Update']); $form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

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

View File

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

View File

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

View File

@@ -22,52 +22,6 @@ use Symfony\Component\Security\Core\Role\Role;
*/ */
final class ActivityControllerTest extends WebTestCase 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() public function getSecuredPagesAuthenticated()
{ {
self::bootKernel(); self::bootKernel();
@@ -101,6 +55,52 @@ 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() public function testCompleteScenario()
{ {
// Create a new client to browse the application // Create a new client to browse the application

View File

@@ -137,64 +137,6 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
self::assertIsArray($actual); 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 * @dataProvider provideDataFindByPerson
*/ */
@@ -349,4 +291,62 @@ 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'], ['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')]]; 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,46 +57,6 @@ final class ActivityVoterTest extends KernelTestCase
$this->prophet = new \Prophecy\Prophet(); $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() public function dataProvider_testVoteAction()
{ {
$centerA = $this->prepareCenter(1, 'center A'); $centerA = $this->prepareCenter(1, 'center A');
@@ -150,6 +110,46 @@ 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. * prepare a token interface with correct rights.
* *

View File

@@ -30,18 +30,6 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown(); 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 public static function generateAsideActivityId(): iterable
{ {
self::bootKernel(); self::bootKernel();
@@ -70,6 +58,18 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown(); 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() public function testIndexWithoutUsers()
{ {
self::ensureKernelShutdown(); self::ensureKernelShutdown();

View File

@@ -24,11 +24,7 @@ use Doctrine\ORM\EntityManagerInterface;
class CalendarForShortMessageProvider class CalendarForShortMessageProvider
{ {
public function __construct( public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {}
private readonly CalendarRepository $calendarRepository,
private readonly EntityManagerInterface $em,
private readonly RangeGeneratorInterface $rangeGenerator,
) {}
/** /**
* Generate calendars instance. * Generate calendars instance.

View File

@@ -42,32 +42,6 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown(); 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 public static function provideAccompanyingPeriod(): iterable
{ {
self::bootKernel(); self::bootKernel();
@@ -108,4 +82,30 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown(); 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,6 +45,20 @@ class MSUserAbsenceReaderTest extends TestCase
self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message); 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 public static function provideDataTestUserAbsence(): iterable
{ {
// contains data that was retrieved from microsoft graph api on 2023-07-06 // contains data that was retrieved from microsoft graph api on 2023-07-06
@@ -159,18 +173,4 @@ class MSUserAbsenceReaderTest extends TestCase
'User is absent: absence is always enabled', '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

@@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider; use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface; use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -81,16 +82,10 @@ final class CalendarForShortMessageProviderTest extends TestCase
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled(); $em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider( $provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(), $calendarRepository->reveal(),
$em->reveal(), $em->reveal(),
$calendarRangeGenerator->reveal(), new DefaultRangeGenerator()
); );
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
@@ -108,32 +103,26 @@ final class CalendarForShortMessageProviderTest extends TestCase
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type('int'), Argument::type('int'),
Argument::exact(0) Argument::exact(0)
)->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1); )->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1);
$calendarRepository->findByNotificationAvailable( $calendarRepository->findByNotificationAvailable(
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type('int'), Argument::type('int'),
Argument::exact(10) Argument::not(0)
)->will(static fn ($args) => [])->shouldBeCalledTimes(1); )->will(static fn ($args) => [])->shouldBeCalledTimes(1);
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled(); $em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider( $provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(), $calendarRepository->reveal(),
$em->reveal(), $em->reveal(),
$calendarRangeGenerator->reveal(), new DefaultRangeGenerator()
); );
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
$this->assertEquals(10, \count($calendars)); $this->assertEquals(1, \count($calendars));
$this->assertContainsOnly(Calendar::class, $calendars); $this->assertContainsOnly(Calendar::class, $calendars);
} }
} }

View File

@@ -28,24 +28,6 @@ use PHPUnit\Framework\TestCase;
*/ */
final class DefaultRangeGeneratorTest extends 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. * * Lundi => Envoi des rdv du mardi et mercredi.
* * Mardi => Envoi des rdv du jeudi. * * Mardi => Envoi des rdv du jeudi.
@@ -97,4 +79,22 @@ final class DefaultRangeGeneratorTest extends TestCase
null, 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,6 +49,80 @@ final class CustomFieldsChoiceTest extends KernelTestCase
parent::tearDown(); 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 * Test if the representation of the data is deserialized to an array text
* with an "allow_other" field. * with an "allow_other" field.
@@ -338,58 +412,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertTrue($isEmpty); $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 // test function isEmptyValue
@@ -413,28 +435,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertFalse($isEmpty); $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 * @param array $options
* *

View File

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

View File

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

View File

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

View File

@@ -1,132 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\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,36 +31,6 @@ final class DocGenEncoderTest extends TestCase
$this->encoder = new DocGenEncoder(); $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() public static function generateEncodeData()
{ {
yield [['tests' => 'ok'], ['tests' => 'ok'], 'A simple test with a simple array']; yield [['tests' => 'ok'], ['tests' => 'ok'], 'A simple test with a simple array'];
@@ -123,4 +93,34 @@ final class DocGenEncoderTest extends TestCase
'a longer list, with near real data inside and embedded associative arrays', '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,2 +1,4 @@
docgen: docgen:
# No ICU messages needed for data_dump_email anymore data_dump_email:
link_valid_until: >-
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}

View File

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

View File

@@ -85,69 +85,6 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
self::assertEquals($expected, $urlGenerator->validateSignature($signature, $method, $objectName, $expiration), $message); 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 * @dataProvider generateValidateSignaturePostData
*/ */
@@ -227,6 +164,69 @@ 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 private function buildGenerator(?UrlGeneratorInterface $urlGenerator = null, ?ClockInterface $clock = null): TempUrlLocalStorageGenerator
{ {
return new TempUrlLocalStorageGenerator( return new TempUrlLocalStorageGenerator(

View File

@@ -31,20 +31,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/ */
final class StoredObjectManagerTest extends TestCase 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 public static function getDataProviderForRead(): \Generator
{ {
/* HAPPY SCENARIO */ /* HAPPY SCENARIO */
@@ -110,40 +96,6 @@ 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 public static function getDataProviderForWrite(): \Generator
{ {
/* HAPPY SCENARIO */ /* HAPPY SCENARIO */
@@ -198,6 +150,54 @@ 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 public function testDelete(): void
{ {
$storedObject = new StoredObject(); $storedObject = new StoredObject();

View File

@@ -82,38 +82,6 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertEquals($expected, $signedUrl); 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 * @dataProvider dataProviderGeneratePost
*/ */
@@ -157,6 +125,38 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix)); 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 public static function dataProviderGeneratePost(): iterable
{ {
$now = \DateTimeImmutable::createFromFormat('U', '1702041743'); $now = \DateTimeImmutable::createFromFormat('U', '1702041743');

View File

@@ -61,55 +61,6 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
$controller->contentOperate($request); $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 public function testOperateContentGetHappyScenario(): void
{ {
$objectName = 'testABC'; $objectName = 'testABC';
@@ -335,4 +286,53 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
'Filename does not start with signed prefix', '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,6 +136,63 @@ class WebdavControllerTest extends KernelTestCase
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); 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 public static function generateDataPropfindDocument(): iterable
{ {
$content = $content =
@@ -290,25 +347,6 @@ 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 public static function generateDataPropfindDirectory(): iterable
{ {
yield [ yield [
@@ -376,44 +414,6 @@ class WebdavControllerTest extends KernelTestCase
'test creatableContentsInfo', '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 class MockedStoredObjectManager implements StoredObjectManagerInterface

View File

@@ -87,16 +87,6 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
self::assertIsInt($nb, 'test that the query could be executed'); 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 * @dataProvider provideDateForFetchQueryForAccompanyingPeriod
*/ */
@@ -152,4 +142,14 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
yield [$period, null, null, 'test']; yield [$period, null, null, 'test'];
yield [$period, new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), '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,6 +50,19 @@ class StoredObjectVoterTest extends TestCase
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute])); 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 public static function provideDataVote(): iterable
{ {
yield [ yield [
@@ -107,17 +120,4 @@ class StoredObjectVoterTest extends TestCase
VoterInterface::ACCESS_GRANTED, 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)); self::assertEquals($expected, $cronJob->canRun($cronJobExecution));
} }
public static function buildTestCanRunData(): iterable
{
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))),
true,
];
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))),
true,
];
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))),
false,
];
yield [
null,
true,
];
}
public function testRun(): void public function testRun(): void
{ {
// we create a clock in the future. This led us a chance to having stored object to delete // we create a clock in the future. This led us a chance to having stored object to delete
@@ -86,6 +63,29 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
self::assertIsInt($results['last-deleted-stored-object-version-id']); self::assertIsInt($results['last-deleted-stored-object-version-id']);
} }
public static function buildTestCanRunData(): iterable
{
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))),
true,
];
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))),
true,
];
yield [
(new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))),
false,
];
yield [
null,
true,
];
}
private function buildMessageBus(bool $expectDistpatchAtLeastOnce = false): MessageBusInterface private function buildMessageBus(bool $expectDistpatchAtLeastOnce = false): MessageBusInterface
{ {
$messageBus = $this->createMock(MessageBusInterface::class); $messageBus = $this->createMock(MessageBusInterface::class);

View File

@@ -48,6 +48,30 @@ 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. * Displays a form to edit an existing EventType entity.
*/ */
@@ -63,10 +87,12 @@ class EventTypeController extends AbstractController
} }
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/edit.html.twig', [ return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -100,6 +126,28 @@ 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. * Edits an existing EventType entity.
*/ */
@@ -114,6 +162,7 @@ class EventTypeController extends AbstractController
throw $this->createNotFoundException('Unable to find EventType entity.'); throw $this->createNotFoundException('Unable to find EventType entity.');
} }
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$editForm->handleRequest($request); $editForm->handleRequest($request);
@@ -126,6 +175,7 @@ class EventTypeController extends AbstractController
return $this->render('@ChillEvent/EventType/edit.html.twig', [ return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -148,6 +198,22 @@ class EventTypeController extends AbstractController
return $form; 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. * Creates a form to edit a EventType entity.
* *
@@ -162,7 +228,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_update', 'chill_eventtype_admin_update',
['id' => $entity->getId()] ['id' => $entity->getId()]
), ),
'method' => 'POST', 'method' => 'PUT',
]); ]);
$form->add('submit', SubmitType::class, ['label' => 'Update']); $form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,6 +48,30 @@ 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. * Displays a form to edit an existing Role entity.
*/ */
@@ -63,10 +87,12 @@ class RoleController extends AbstractController
} }
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/edit.html.twig', [ return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -100,6 +126,28 @@ 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. * Edits an existing Role entity.
*/ */
@@ -114,6 +162,7 @@ class RoleController extends AbstractController
throw $this->createNotFoundException('Unable to find Role entity.'); throw $this->createNotFoundException('Unable to find Role entity.');
} }
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$editForm->handleRequest($request); $editForm->handleRequest($request);
@@ -126,6 +175,7 @@ class RoleController extends AbstractController
return $this->render('@ChillEvent/Role/edit.html.twig', [ return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -148,6 +198,20 @@ class RoleController extends AbstractController
return $form; 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. * Creates a form to edit a Role entity.
* *
@@ -162,7 +226,7 @@ class RoleController extends AbstractController
'chill_event_admin_role_update', 'chill_event_admin_role_update',
['id' => $entity->getId()] ['id' => $entity->getId()]
), ),
'method' => 'POST', 'method' => 'PUT',
]); ]);
$form->add('submit', SubmitType::class, ['label' => 'Update']); $form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,6 +48,30 @@ 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. * Displays a form to edit an existing Status entity.
*/ */
@@ -63,10 +87,12 @@ class StatusController extends AbstractController
} }
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/edit.html.twig', [ return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -100,6 +126,28 @@ 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. * Edits an existing Status entity.
*/ */
@@ -114,6 +162,7 @@ class StatusController extends AbstractController
throw $this->createNotFoundException('Unable to find Status entity.'); throw $this->createNotFoundException('Unable to find Status entity.');
} }
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity); $editForm = $this->createEditForm($entity);
$editForm->handleRequest($request); $editForm->handleRequest($request);
@@ -126,6 +175,7 @@ class StatusController extends AbstractController
return $this->render('@ChillEvent/Status/edit.html.twig', [ return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity, 'entity' => $entity,
'edit_form' => $editForm->createView(), 'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]); ]);
} }
@@ -148,6 +198,19 @@ class StatusController extends AbstractController
return $form; 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. * Creates a form to edit a Status entity.
* *
@@ -159,7 +222,7 @@ class StatusController extends AbstractController
{ {
$form = $this->createForm(StatusType::class, $entity, [ $form = $this->createForm(StatusType::class, $entity, [
'action' => $this->generateUrl('chill_event_admin_status_update', ['id' => $entity->getId()]), 'action' => $this->generateUrl('chill_event_admin_status_update', ['id' => $entity->getId()]),
'method' => 'POST', 'method' => 'PUT',
]); ]);
$form->add('submit', SubmitType::class, ['label' => 'Update']); $form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -54,14 +54,14 @@ block js %}
{% if e.participations|length > 0 %} {% if e.participations|length > 0 %}
<div class="item-row separator"> <div class="item-row separator">
<strong>{{ "Participations" | trans }}&nbsp;: </strong> <strong>{{ "Participations" | trans }}&nbsp;: </strong>
{% for part in e.participations|slice(0, 5) %} {% include {% for part in e.participations|slice(0, 20) %} {% include
'@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: part.person.id }, action: targetEntity: { name: 'person', id: part.person.id }, action:
'show', displayBadge: true, buttonText: 'show', displayBadge: true, buttonText:
part.person|chill_entity_render_string, isDead: part.person|chill_entity_render_string, isDead:
part.person.deathdate is not null } %} {% endfor %} part.person.deathdate is not null } %} {% endfor %} {% if
{% if e.participations|length > 5 %} e.participations|length > 20 %}
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 5}) }} {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,6 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompiler
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass; use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass; use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Chill\MainBundle\Notification\NotificationHandlerInterface; use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface; use Chill\MainBundle\Search\SearchApiInterface;
@@ -62,8 +61,6 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.entity_info_provider'); ->addTag('chill_main.entity_info_provider');
$container->registerForAutoconfiguration(ProvideRoleInterface::class) $container->registerForAutoconfiguration(ProvideRoleInterface::class)
->addTag('chill_main.provide_role'); ->addTag('chill_main.provide_role');
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
->addTag('chill_main.notification_flag_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);

View File

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

View File

@@ -16,7 +16,6 @@ use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Form\NotificationCommentType; use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Form\NotificationType; use Chill\MainBundle\Form\NotificationType;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound; use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
use Chill\MainBundle\Notification\NotificationHandlerManager; use Chill\MainBundle\Notification\NotificationHandlerManager;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository; use Chill\MainBundle\Repository\NotificationRepository;
@@ -58,8 +57,7 @@ class NotificationController extends AbstractController
$notification $notification
->setRelatedEntityClass($request->query->get('entityClass')) ->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId')) ->setRelatedEntityId($request->query->getInt('entityId'))
->setSender($this->security->getUser()) ->setSender($this->security->getUser());
->setType(NotificationByUserFlagProvider::FLAG);
$tos = $request->query->all('tos'); $tos = $request->query->all('tos');

View File

@@ -11,11 +11,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\UserProfileType; use Chill\MainBundle\Form\UserPhonenumberType;
use Chill\MainBundle\Security\ChillSecurity; use Chill\MainBundle\Security\ChillSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -38,19 +41,16 @@ final class UserProfileController extends AbstractController
} }
$user = $this->security->getUser(); $user = $this->security->getUser();
$editForm = $this->createForm(UserProfileType::class, $user); $editForm = $this->createPhonenumberEditForm($user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->handleRequest($request); $editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
$notificationFlagsData = $editForm->get('notificationFlags')->getData(); $phonenumber = $editForm->get('phonenumber')->getData();
$user->setNotificationFlags($notificationFlagsData);
$em = $this->managerRegistry->getManager(); $user->setPhonenumber($phonenumber);
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!')); $this->managerRegistry->getManager()->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile'); return $this->redirectToRoute('chill_main_user_profile');
} }
@@ -60,4 +60,13 @@ final class UserProfileController extends AbstractController
'form' => $editForm->createView(), 'form' => $editForm->createView(),
]); ]);
} }
private function createPhonenumberEditForm(UserInterface $user): FormInterface
{
return $this->createForm(
UserPhonenumberType::class,
$user,
)
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
}
} }

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -22,10 +21,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity] #[ORM\Entity]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_main_notification')] #[ORM\Table(name: 'chill_main_notification')]
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')] #[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
class Notification implements TrackUpdateInterface class Notification implements TrackUpdateInterface
{ {
#[ORM\Column(type: Types::TEXT, nullable: false)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
private string $accessKey; private string $accessKey;
private array $addedAddresses = []; private array $addedAddresses = [];
@@ -37,19 +36,12 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')] #[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
private Collection $addressees; private Collection $addressees;
/**
* @var Collection<int, UserGroup>
*/
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
#[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
private Collection $addresseeUserGroups;
/** /**
* a list of destinee which will receive notifications. * a list of destinee which will receive notifications.
* *
* @var array|string[] * @var array|string[]
*/ */
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $addressesEmails = []; private array $addressesEmails = [];
/** /**
@@ -68,21 +60,21 @@ class Notification implements TrackUpdateInterface
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])] #[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $comments; private Collection $comments;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $date; private \DateTimeImmutable $date;
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(type: Types::TEXT)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $message = ''; private string $message = '';
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
private string $relatedEntityClass = ''; private string $relatedEntityClass = '';
#[ORM\Column(type: Types::INTEGER)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private int $relatedEntityId; private int $relatedEntityId;
private array $removedAddresses = []; private array $removedAddresses = [];
@@ -92,7 +84,7 @@ class Notification implements TrackUpdateInterface
private ?User $sender = null; private ?User $sender = null;
#[Assert\NotBlank(message: 'notification.Title must be defined')] #[Assert\NotBlank(message: 'notification.Title must be defined')]
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
private string $title = ''; private string $title = '';
/** /**
@@ -102,46 +94,31 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')] #[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
private Collection $unreadBy; private Collection $unreadBy;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
private ?\DateTimeImmutable $updatedAt = null; private ?\DateTimeImmutable $updatedAt = null;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
private ?User $updatedBy = null; private ?User $updatedBy = null;
#[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
private string $type = '';
public function __construct() public function __construct()
{ {
$this->addressees = new ArrayCollection(); $this->addressees = new ArrayCollection();
$this->addresseeUserGroups = new ArrayCollection();
$this->unreadBy = new ArrayCollection(); $this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection(); $this->comments = new ArrayCollection();
$this->setDate(new \DateTimeImmutable()); $this->setDate(new \DateTimeImmutable());
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
} }
public function addAddressee(User|UserGroup $addressee): self public function addAddressee(User $addressee): self
{ {
if ($addressee instanceof User) { if (!$this->addressees->contains($addressee)) {
if (!$this->addressees->contains($addressee)) { $this->addressees[] = $addressee;
$this->addressees->add($addressee); $this->addedAddresses[] = $addressee;
$this->addedAddresses[] = $addressee;
}
return $this;
}
if (!$this->addresseeUserGroups->contains($addressee)) {
$this->addresseeUserGroups->add($addressee);
} }
return $this; return $this;
} }
/**
* @deprecated
*/
public function addAddressesEmail(string $email) public function addAddressesEmail(string $email)
{ {
if (!\in_array($email, $this->addressesEmails, true)) { if (!\in_array($email, $this->addressesEmails, true)) {
@@ -175,23 +152,13 @@ class Notification implements TrackUpdateInterface
#[Assert\Callback] #[Assert\Callback]
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
{ {
if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) { if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
$context->buildViolation('notification.At least one addressee') $context->buildViolation('notification.At least one addressee')
->atPath('addressees') ->atPath('addressees')
->addViolation(); ->addViolation();
} }
} }
public function getAddresseeUserGroups(): Collection
{
return $this->addresseeUserGroups;
}
public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
{
$this->addresseeUserGroups = $addresseeUserGroups;
}
public function getAccessKey(): string public function getAccessKey(): string
{ {
return $this->accessKey; return $this->accessKey;
@@ -215,23 +182,6 @@ class Notification implements TrackUpdateInterface
return $this->addressees; return $this->addressees;
} }
public function getAllAddressees(): array
{
$allUsers = [];
foreach ($this->getAddressees() as $user) {
$allUsers[$user->getId()] = $user;
}
foreach ($this->getAddresseeUserGroups() as $userGroup) {
foreach ($userGroup->getUsers() as $user) {
$allUsers[$user->getId()] = $user;
}
}
return array_values($allUsers);
}
/** /**
* @return array|string[] * @return array|string[]
*/ */
@@ -353,18 +303,12 @@ class Notification implements TrackUpdateInterface
$this->addressesOnLoad = null; $this->addressesOnLoad = null;
} }
public function removeAddressee(User|UserGroup $addressee): self public function removeAddressee(User $addressee): self
{ {
if ($addressee instanceof User) { if ($this->addressees->removeElement($addressee)) {
if ($this->addressees->contains($addressee)) { $this->removedAddresses[] = $addressee;
$this->addressees->removeElement($addressee);
return $this;
}
} }
$this->addresseeUserGroups->removeElement($addressee);
return $this; return $this;
} }
@@ -434,7 +378,7 @@ class Notification implements TrackUpdateInterface
public function setUpdatedAt(\DateTimeInterface $datetime): self public function setUpdatedAt(\DateTimeInterface $datetime): self
{ {
$this->updatedAt = \DateTimeImmutable::createFromInterface($datetime); $this->updatedAt = $datetime;
return $this; return $this;
} }
@@ -445,16 +389,4 @@ class Notification implements TrackUpdateInterface
return $this; return $this;
} }
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getType(): string
{
return $this->type;
}
} }

View File

@@ -176,14 +176,6 @@ 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. * Determines if the user is shared with either directly or through a group.
* *

View File

@@ -34,9 +34,6 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
#[ORM\Table(name: 'users')] #[ORM\Table(name: 'users')]
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
{ {
public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
#[ORM\Id] #[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')] #[ORM\GeneratedValue(strategy: 'AUTO')]
@@ -119,12 +116,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[PhonenumberConstraint] #[PhonenumberConstraint]
private ?PhoneNumber $phonenumber = null; private ?PhoneNumber $phonenumber = null;
/**
* @var array<string, list<string>>
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $notificationFlags = [];
/** /**
* User constructor. * User constructor.
*/ */
@@ -622,57 +613,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this; return $this;
} }
/**
* Check if the current object is an instance of User.
*
* @return bool returns true if the current object is an instance of User, false otherwise
*/
public function isUser(): bool
{
return true;
}
public function getNotificationFlags(): array
{
return $this->notificationFlags;
}
public function setNotificationFlags(array $notificationFlags)
{
$this->notificationFlags = $notificationFlags;
}
public function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [];
}
public function setNotificationFlagData(string $flag, array $data): void
{
$this->notificationFlags[$flag] = $data;
}
public function isNotificationSendImmediately(string $type): bool
{
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function isNotificationDailyDigest(string $type): bool
{
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function getLocale(): string
{
return 'fr';
}
} }

View File

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

View File

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

View File

@@ -0,0 +1,241 @@
<?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

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

View File

@@ -13,7 +13,6 @@ namespace Chill\MainBundle\Export\Formatter;
use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportManagerAwareInterface; use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormattedExportGeneration;
use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Date;
@@ -22,9 +21,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
@@ -34,17 +31,42 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
{ {
use ExportManagerAwareTrait; use ExportManagerAwareTrait;
public function __construct(private readonly TranslatorInterface $translator) {} 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 * build a form, which will be used to collect data required for the execution
* of this formatter. * of this formatter.
* *
* @uses appendAggregatorForm * @uses appendAggregatorForm
*
* @param string $exportAlias
*/ */
public function buildForm( public function buildForm(
FormBuilderInterface $builder, FormBuilderInterface $builder,
string $exportAlias, $exportAlias,
array $aggregatorAliases, array $aggregatorAliases,
): void { ): void {
$builder $builder
@@ -86,96 +108,11 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
return ['numerotation' => true, 'format' => 'xlsx']; return ['numerotation' => true, 'format' => 'xlsx'];
} }
public function getName(): string|TranslatableInterface public function getName(): string|\Symfony\Contracts\Translation\TranslatableInterface
{ {
return 'Spreadsheet list formatter (.xlsx, .ods)'; return 'Spreadsheet list formatter (.xlsx, .ods)';
} }
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($cacheLabels, $worksheet, $result, $formatterData, $exportAlias, $exportData);
$i = 1;
foreach ($result as $row) {
if (true === $formatterData['numerotation']) {
$worksheet->setCellValue('A'.($i + 1), (string) $i);
}
$a = $formatterData['numerotation'] ? 'B' : 'A';
foreach ($row as $key => $value) {
$row = $a.($i + 1);
$formattedValue = $this->getLabel($cacheLabels, $key, $value, $result, $exportAlias, $exportData);
if ($formattedValue instanceof \DateTimeInterface) {
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
if ('000000' === $formattedValue->format('His')) {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY);
} else {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
}
} elseif ($formattedValue instanceof TranslatableInterface) {
$worksheet->setCellValue($row, $formattedValue->trans($this->translator));
} else {
$worksheet->setCellValue($row, $formattedValue);
}
++$a;
}
++$i;
}
switch ($formatterData['format']) {
case 'ods':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods');
$contentType = 'application/vnd.oasis.opendocument.spreadsheet';
break;
case 'xlsx':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$contentType = 'application/vnd.openxmlformats-officedocument.'
.'spreadsheetml.sheet';
break;
case 'csv':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv');
$contentType = 'text/csv';
break;
default:
// this should not happen
// throw an exception to ensure that the error is catched
throw new \OutOfBoundsException('The format '.$formatterData['format'].' is not supported');
}
$tempfile = \tempnam(\sys_get_temp_dir(), '');
$writer->save($tempfile);
$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. * Generate a response from the data collected on differents ExportElementInterface.
* *
@@ -196,10 +133,89 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
array $aggregatorsData, array $aggregatorsData,
ExportGenerationContext $context, ExportGenerationContext $context,
) { ) {
$generated = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context); $this->result = $result;
$this->exportAlias = $exportAlias;
$this->exportData = $exportData;
$this->formatterData = $formatterData;
$response = new BinaryFileResponse($generated->content); $spreadsheet = new Spreadsheet();
$response->headers->set('Content-Type', $generated->contentType); $worksheet = $spreadsheet->getActiveSheet();
$this->prepareHeaders($worksheet);
$i = 1;
foreach ($result as $row) {
if (true === $this->formatterData['numerotation']) {
$worksheet->setCellValue('A'.($i + 1), (string) $i);
}
$a = $this->formatterData['numerotation'] ? 'B' : 'A';
foreach ($row as $key => $value) {
$row = $a.($i + 1);
$formattedValue = $this->getLabel($key, $value);
if ($formattedValue instanceof \DateTimeInterface) {
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
if ('000000' === $formattedValue->format('His')) {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY);
} else {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
}
} else {
$worksheet->setCellValue($row, $formattedValue);
}
++$a;
}
++$i;
}
switch ($this->formatterData['format']) {
case 'ods':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods');
$contentType = 'application/vnd.oasis.opendocument.spreadsheet';
break;
case 'xlsx':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$contentType = 'application/vnd.openxmlformats-officedocument.'
.'spreadsheetml.sheet';
break;
case 'csv':
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv');
$contentType = 'text/csv';
break;
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');
}
$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);
// remove the temp file from disk
\unlink($tempfile);
return $response; return $response;
} }
@@ -212,29 +228,34 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
/** /**
* Give the label corresponding to the given key and value. * Give the label corresponding to the given key and value.
* *
* @return string|\DateTimeInterface|int|float|TranslatableInterface|null * @param string $key
* @param string $value
*
* @return string
* *
* @throws \LogicException if the label is not found * @throws \LogicException if the label is not found
*/ */
private function getLabel(array $labelsCache, $key, $value, array $result, string $exportAlias, array $exportData) protected function getLabel($key, $value)
{ {
if (!\array_key_exists($key, $labelsCache)) { if (null === $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($labelsCache)))); $this->prepareCacheLabels();
} }
return $labelsCache[$key]($value); 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. * Prepare the label cache which will be used by getLabel. This function
* * should be called only once in the generation lifecycle.
* @return array The labels cache
*/ */
private function prepareCacheLabels(array $result, string $exportAlias, array $exportData): array protected function prepareCacheLabels()
{ {
$labelsCache = []; $export = $this->getExportManager()->getExport($this->exportAlias);
$export = $this->getExportManager()->getExport($exportAlias); $keys = $export->getQueryKeys($this->exportData);
$keys = $export->getQueryKeys($exportData);
foreach ($keys as $key) { foreach ($keys as $key) {
// get an array with all values for this key if possible // get an array with all values for this key if possible
@@ -244,31 +265,29 @@ class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAware
} }
return $v[$key]; return $v[$key];
}, $result); }, $this->result);
// store the label in the labelsCache // store the label in the labelsCache property
$labelsCache[$key] = $export->getLabels($key, $values, $exportData); $this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
} }
return $labelsCache;
} }
/** /**
* add the headers to the csv file. * add the headers to the csv file.
*/ */
protected function prepareHeaders(array $labelsCache, Worksheet $worksheet, array $result, array $formatterData, string $exportAlias, array $exportData) protected function prepareHeaders(Worksheet $worksheet)
{ {
$keys = $this->getExportManager()->getExport($exportAlias)->getQueryKeys($exportData); $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 // we want to keep the order of the first row. So we will iterate on the first row of the results
$first_row = \count($result) > 0 ? $result[0] : []; $first_row = \count($this->result) > 0 ? $this->result[0] : [];
$header_line = []; $header_line = [];
if (true === $formatterData['numerotation']) { if (true === $this->formatterData['numerotation']) {
$header_line[] = $this->translator->trans('Number'); $header_line[] = $this->translator->trans('Number');
} }
foreach ($first_row as $key => $value) { foreach ($first_row as $key => $value) {
$header_line[] = $this->translator->trans( $header_line[] = $this->translator->trans(
$this->getLabel($labelsCache, $key, '_header', $result, $exportAlias, $exportData) $this->getLabel($key, '_header')
); );
} }

View File

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

View File

@@ -1,75 +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\Form\DataMapper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
public function __construct(private array $notificationFlagProviders) {}
public function mapDataToForms($viewData, $forms): void
{
if (null === $viewData) {
$viewData = [];
}
$formsArray = iterator_to_array($forms);
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|| !array_key_exists($flag, $viewData);
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
if ($flagForm->has('immediate_email')) {
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
}
if ($flagForm->has('daily_email')) {
$flagForm->get('daily_email')->setData($dailyEmailChecked);
}
}
}
}
public function mapFormsToData($forms, &$viewData): void
{
$formsArray = iterator_to_array($forms);
$viewData = [];
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$viewData[$flag] = [];
if (true === $flagForm['immediate_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
if (true === $flagForm['daily_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
}
if ([] === $viewData[$flag]) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
}
}
}
}

View File

@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form; namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType class NotificationType extends AbstractType
{ {
@@ -28,14 +33,29 @@ class NotificationType extends AbstractType
'label' => 'Title', 'label' => 'Title',
'required' => true, 'required' => true,
]) ])
->add('addressees', PickUserGroupOrUserDynamicType::class, [ ->add('addressees', PickUserDynamicType::class, [
'multiple' => true, 'multiple' => true,
'label' => 'notification.Pick user or user group', 'required' => false,
'empty_data' => '[]',
'required' => true,
]) ])
->add('message', ChillTextareaType::class, [ ->add('message', ChillTextareaType::class, [
'required' => false, 'required' => false,
])
->add('addressesEmails', ChillCollectionType::class, [
'label' => 'notification.dest by email',
'help' => 'notification.dest by email help',
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'notification.Add an email',
'button_remove_label' => 'notification.Remove an email',
'empty_collection_explain' => 'notification.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(),
],
'label' => 'Email',
],
]); ]);
} }

View File

@@ -55,10 +55,6 @@ class DateIntervalType extends AbstractType
{ {
$builder $builder
->add('n', IntegerType::class, [ ->add('n', IntegerType::class, [
'attr' => [
'min' => 0,
'step' => 1,
],
'constraints' => [ 'constraints' => [
new GreaterThan([ new GreaterThan([
'value' => 0, 'value' => 0,

View File

@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NotificationFlagsType extends AbstractType
{
private readonly array $notificationFlagProviders;
public function __construct(NotificationFlagManager $notificationFlagManager)
{
$this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
$builder->add($flag, FormType::class, [
'label' => $flagProvider->getLabel(),
'required' => false,
]);
$builder->get($flag)
->add('immediate_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
->add('daily_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@@ -1,102 +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\Notification\Email;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class DailyNotificationDigestCronjob implements CronJobInterface
{
public function __construct(
private ClockInterface $clock,
private Connection $connection,
private MessageBusInterface $messageBus,
private LoggerInterface $logger,
) {}
public function canRun(?CronJobExecution $cronJobExecution): bool
{
$now = $this->clock->now();
if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
return false;
}
// Run between 6 and 9 AM
return in_array((int) $now->format('H'), [6, 7, 8], true);
}
public function getKey(): string
{
return 'daily-notification-digest';
}
/**
* @throws \DateInvalidOperationException
* @throws Exception
*/
public function run(array $lastExecutionData): ?array
{
$now = $this->clock->now();
if (isset($lastExecutionData['last_execution'])) {
$lastExecution = \DateTimeImmutable::createFromFormat(
\DateTimeImmutable::ATOM,
$lastExecutionData['last_execution']
);
} else {
$lastExecution = $now->sub(new \DateInterval('P1D'));
}
// Get distinct users who received notifications since the last execution
$sql = <<<'SQL'
SELECT DISTINCT cmnau.user_id
FROM chill_main_notification cmn
JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
WHERE cmn.date >= :lastExecution AND cmn.date <= :now
SQL;
$sqlStatement = $this->connection->prepare($sql);
$sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
$sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
$result = $sqlStatement->executeQuery();
$count = 0;
foreach ($result->fetchAllAssociative() as $row) {
$userId = (int) $row['user_id'];
$message = new ScheduleDailyNotificationDigestMessage(
$userId,
$lastExecution,
$now
);
$this->messageBus->dispatch($message);
++$count;
}
$this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
'user_count' => $count,
'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
'current_time' => $now->format('Y-m-d-H:i:s.u e'),
]);
return [
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
];
}
}

View File

@@ -1,75 +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\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class ScheduleDailyNotificationDigestHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
*/
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
{
$userId = $message->getUserId();
$lastExecutionDate = $message->getLastExecutionDateTime();
$currentDate = $message->getCurrentDateTime();
$user = $this->userRepository->find($userId);
if (null === $user) {
$this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
'user_id' => $userId,
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
}
// Get all notifications for this user between last execution and current date
$notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
$userId,
$lastExecutionDate,
$currentDate
);
// Filter out notifications that should be sent in a daily digest
$dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
if ([] === $dailyNotifications) {
$this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
'user_id' => $userId,
]);
return;
}
$this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
$this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
'user_id' => $userId,
'notification_count' => count($dailyNotifications),
]);
}
}

View File

@@ -1,68 +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\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class SendImmediateNotificationEmailHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
* @throws \Exception
*/
public function __invoke(SendImmediateNotificationEmailMessage $message): void
{
$notification = $this->notificationRepository->find($message->getNotificationId());
$addressee = $this->userRepository->find($message->getAddresseeId());
if (null === $notification) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
'notification_id' => $message->getNotificationId(),
]);
throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
}
if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
'addressee_id' => $message->getAddresseeId(),
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
}
try {
$this->notificationMailer->sendEmailToAddressee($notification, $addressee);
} catch (\Exception $e) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
'notification_id' => $message->getNotificationId(),
'addressee_id' => $message->getAddresseeId(),
'stacktrace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}

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