Compare commits

..

91 Commits

Author SHA1 Message Date
f55dee4f8a Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-12-05 17:51:16 +01:00
1bbea33c4b Define the slots in the Modal.vue 2025-11-20 08:36:36 +01:00
310dd61870 Simplify the slot organisation to avoid multiple slots declaration 2025-11-20 08:36:22 +01:00
4ddbcf4037 Refactor SCSS and Vue components to standardize "required" labels.
- Moved SCSS rules for `.required` labels to a shared scope in `forms.scss`.
- Updated labels in `PersonEdit.vue` to consistently use the `required` class.
2025-11-19 16:45:15 +01:00
6d3bcf48b5 Refactor BannerComponent.vue to handle caller data via a computed property and define type-checking utility for Thirdparty.
- Replaced inline caller logic with a reusable `caller` computed property in `BannerComponent.vue`.
- Added `isThirdparty` and `isBaseThirdParty` utility functions to validate `Thirdparty` types in `types.ts`.
2025-11-10 13:49:43 +01:00
7136c682f2 Refactor Person.vue and ThirdParty.vue to enhance data loading and improve conditional rendering.
- Added `loadData` method in `Person.vue` to handle person data fetching based on props.
- Improved `ThirdParty.vue` by refining conditional checks and updating prop types for better maintainability.
2025-11-07 17:21:03 +01:00
625e55056d Add translation keys and update components for accompanying course actions
- Added new translation keys for requestor (`add_requestor`), associated persons, and resources in `messages.fr.yml`.
- Refactored Vue components (`Requestor.vue`, `Resources.vue`, `PersonsAssociated.vue`) to use these new translations dynamically.
- Imported necessary constants and the `trans` method for translation handling.
2025-11-07 17:20:54 +01:00
6d617146b8 Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-11-07 14:03:10 +01:00
14e0cba2e8 Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-11-07 11:12:48 +01:00
627e128977 Merge branch 'ticket-app-master' into ticket/64-identifiants-person
# Conflicts:
#	package.json
#	src/Bundle/ChillMainBundle/Entity/User.php
#	src/Bundle/ChillMainBundle/Resources/public/types.ts
#	src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue
#	src/Bundle/ChillPersonBundle/chill.api.specs.yaml
#	src/Bundle/ChillTicketBundle/src/Messenger/Handler/PostTicketUpdateMessageHandler.php
2025-11-06 11:23:34 +01:00
3f6bbbc1b3 Display a message when no results are found in PersonChooseModal and refine search state handling.
- Added a "no results" message with dynamic translation when no suggestions are available.
- Introduced `hasPreviousQuery` state and `hasNoResult` computed property for improved search state management.
- Updated styles for "no results" display and adjusted button margin in the modal.
2025-10-29 16:52:40 +01:00
31e57d7507 Display a message when no results are found in PersonChooseModal and refine search state handling.
- Added a "no results" message with dynamic translation when no suggestions are available.
- Introduced `hasPreviousQuery` state and `hasNoResult` computed property for improved search state management.
- Updated styles for "no results" display and adjusted button margin in the modal.
2025-10-29 16:47:28 +01:00
5c098a336d Add edit functionality for ThirdParty in OnTheFly.vue and refactor ThirdPartyEdit for consistency.
- Implemented dynamic component rendering in `OnTheFly.vue` for `thirdparty` type based on the `action` (`show`, `edit`, etc.).
- Added `ThirdPartyEdit` component with API integration for editing third parties.
- Introduced `thirdpartyToWriteThirdParty` function in the API for mapping `Thirdparty` to `ThirdPartyWrite` structure.
- Centralized validation handling in `ThirdPartyEdit.vue` using `useViolationList` composable and enhanced template conditions.
- Updated and extended API functions (`patchThirdparty`, etc.) and types to support third-party editing.
2025-10-29 16:29:44 +01:00
e107d20bea Introduce edit functionality for PersonEdit and refactor OnTheFly components for improved modularity.
- Added support for editing a person entity in `PersonEdit.vue` with proper data initialization and API integration.
- Refactored `OnTheFly.vue` to dynamically render components based on the action (`show`, `edit`, etc.).
- Introduced `personToWritePerson` conversion logic for mapping `Person` to `PersonWrite` structure.
- Enhanced template conditions and loading states for `PersonEdit`.
- Updated API with `editPerson` function and adjusted related types for consistency.
2025-10-29 15:05:08 +01:00
491fd81f9b Fix PersonIdentifier lookup in PersonJsonDenormalizer to use Definition ID comparison.
- Updated lambda function to compare `personIdentifier`'s `Definition` ID instead of its own ID for correct matching.
2025-10-29 15:04:58 +01:00
8c2acbd166 Add support for serializing identifiers in PersonJsonNormalizer and improve PersonIdentifier entity serialization.
- Extended `PersonJsonNormalizer` to include `identifiers` field normalization.
- Added `Serializer` annotations to `PersonIdentifier` and `PersonIdentifierDefinition` for enhanced serialization support.
- Updated `PersonIdentifier` and `PersonIdentifierDefinition` to define serialization groups and discriminator maps.
2025-10-29 15:04:50 +01:00
e291c7abec Refactor ThirdPartyEdit.vue to improve validation handling and streamline input components.
- Replaced individual validation logic with `useViolationList` composable for centralization and consistency.
- Enhanced input components with floating labels, validation error feedback, and improved class binding for better UX.
- Updated API to include `WriteThirdPartyViolationMap` interface for structured validation error mapping.
- Refactored imports and adjusted spacing for better readability and adherence to coding standards.
2025-10-29 12:35:30 +01:00
1e186fab58 Document validation update guidelines for ThirdParty entity to align with associated Vue component logic.
- Added documentation to ensure updates in validations reflect in `ThirdPartyEdit.vue` and violation lists for consistency.
- Clarified the relationship between validation logic and UI component updates.
2025-10-29 12:35:30 +01:00
83a2c04537 Refactor violation handling in PersonEdit.vue by introducing useViolationList composable.
- Centralized violation handling logic with `useViolationList` for improved reusability and maintainability.
- Replaced local violation functions with composable methods in `PersonEdit.vue`.
- Streamlined UI binding for validation errors across multiple inputs.
2025-10-29 12:35:30 +01:00
e89b33bc1a Refactor AddPersons and related components for improved state management and prop handling.
- Replaced array-based `selected` state with `Map` for better key-based selection handling.
- Simplified `PersonSuggestion.vue` and `PersonChooseModal.vue` to align with updated state structure.
- Removed debug logs and legacy code for cleaner and more maintainable codebase.
2025-10-28 15:50:40 +01:00
6b208e9962 Refactor PersonSuggestion and PersonChooseModal for streamlined state management and improved prop handling.
- Replaced `search.suggested` and `search.selected` with dedicated reactive `suggested` and `selected` states.
- Updated `PersonSuggestion.vue` to handle selection via `onUpdateValue` and removed `v-model` usage.
- Simplified `PersonChooseModal.vue` to compute `selectedAndSuggested` directly using props.
- Adjusted `types.ts` to refine `Search.results` and standardize `Suggestion` structure.
2025-10-28 14:32:12 +01:00
b0c63fab91 Refactor AddPersons and related components for consistent state management and improved handling of selected suggestions.
- Replaced `search.selected` with a dedicated `selected` state and implemented explicit methods for adding, removing, and clearing selections.
- Updated event handling and props (`addNewPersons`, `selected`, `updateSelected`, `cleanSelected`) for better separation of concerns and type safety.
- Introduced `isSelected` utility for streamlined selection checks and replaced deprecated event usages.
- Adjusted modal behaviors in `PersonChooseModal.vue` and `AddPersons.vue` for improved integration and alignment with new state logic.
2025-10-28 12:41:52 +01:00
6d4c4d2c74 Refactor entity handling in TypeUser.vue and PickEntity.vue for improved consistency and type safety.
- Adjusted `TypeUser.vue` to correctly reference `props.item.result` and removed unused `hasParent` logic.
- Updated `PickEntity.vue` to include handling for households with new `isEntityHousehold` utility.
- Added `isEntityHousehold` function to `types.ts` for reusable type checks.
2025-10-25 01:06:29 +02:00
df6087d468 Refactor Gender and ResidentialAddress types and update associated components for improved consistency and type safety.
- Replaced `gender` string union with `Gender` type in `Person` interfaces.
- Added `ResidentialAddress` type to standardize address handling within `Person`.
- Updated `PersonRenderBox.vue` to utilize new properties and improve null safety with optional chaining.
- Corrected prop defaults and fixed typo in
2025-10-25 00:58:17 +02:00
ffac143ab9 Update address handling in third-party components and types for consistency.
- Replaced `address_id` with `id` in `ThirdPartyEdit.vue` and associated logic.
- Introduced `SetAddress` type to standardize address references in write operations.
- Updated `SetThirdParty` and related types to use `SetAddress` for better type safety.
2025-10-24 17:03:27 +02:00
f1bf6023ff Refactor third-party handling for consistency and improved data flow.
- Renamed `categories` to `category` in `Thirdparty` and related types for clarity and type safety.
- Updated `ThirdPartyRenderBox.vue` and `CreateModal.vue` to align with new type definitions.
- Enhanced `AddPersons.vue` to handle `onThirdPartyCreated` event properly and extend functionality for third-party creation.
2025-10-24 16:41:08 +02:00
71e146e4f0 fix for ThirdParty create flow.
- Added `parent` property handling in `ThirdPartyEdit.vue`, `Create.vue`, and related components for better association with parent entities.
- Updated `Thirdparty` and `ThirdPartyWrite` types to include `parent` property and ensure type safety.
- Adjusted translations and messages to reflect the new parent entity association concept.
2025-10-24 16:22:09 +02:00
4234377b7e Replace OnTheFly.js with OnTheFly.ts and introduce ThirdPartyEdit.vue for enhanced third-party entity management
- Migrated API functions in `OnTheFly.js` to `OnTheFly.ts` for improved type safety using TypeScript.
- Added `ThirdPartyEdit.vue` to streamline third-party creation and editing with a Vue component structure.
- Updated third-party-related types in `types.ts` to improve consistency and explicitly distinguish `company`, `contact`, and `child` entities.
- Enhanced `AddPersons.vue` and dependent components to support adding third-party contacts.
- Adjusted styles and removed `.btn-tpchild` for consistency in button definitions across SCSS.
2025-10-24 14:21:39 +02:00
1be82b3049 Update PersonIdentifierWorkerNormalizerTest to include presence attribute in expected normalized data
- Replaced `id` with `definition_id` in expected output for consistency.
- Ensured `presence` attribute is validated in test cases.
2025-10-21 15:29:17 +02:00
808954df42 fix implementation of PersonIdentifierEngineInterface in test 2025-10-21 15:18:50 +02:00
3f8bb6c5c0 cs fixes 2025-10-21 15:01:34 +02:00
23e1a0d36a rector fixes 2025-10-21 15:01:21 +02:00
a594d86346 fix cs and missing methods 2025-10-21 14:38:05 +02:00
870907804b Refactor PersonCreate flow to introduce PersonCreateDTO
- Replaced `Person` entity binding with `PersonCreateDTO` in `CreationPersonType` to enable better data handling.
- Added `PersonCreateDTOFactory` for creating and mapping `PersonCreateDTO` instances.
- Extracted `newAction` logic into `PersonCreateController` for clearer separation of responsibilities.
- Updated `PersonIdentifiersDataMapper` and `PersonIdentifierWorker` to support default identifier values.
- Adjusted related services, configurations, and templates accordingly.
2025-10-21 14:24:43 +02:00
e9e6c05e3d Refactor PersonEdit flow to introduce PersonEditDTO
- Replaced `Person` entity binding with `PersonEditDTO` in `PersonType` to decouple data transfer and entity manipulation.
- Added `PersonEditDTOFactory` for creating and mapping `PersonEditDTO` instances.
- Simplified `PersonAltNameDataMapper` and `PersonIdentifiersDataMapper`.
- Updated `PersonEditController` to use `PersonEditDTO` for better separation of concerns.
- Adjusted related tests, configurations, and templates accordingly.
2025-10-21 13:22:04 +02:00
532f2dd842 Add message handler definition on PostTicketUPdateMessageHandler 2025-10-17 00:09:51 +02:00
d14d4d4d8f Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-10-16 14:34:34 +02:00
592a0f3698 Remove FindCallerController 2025-10-16 12:36:44 +02:00
d469eb19ad Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-10-16 10:48:59 +02:00
189a9337b4 Search person by phonenumber: allow searching though identifier and phonenumber 2025-10-07 18:28:59 +02:00
c030232a73 Temporarily desactivate the search by phonenumber 2025-10-07 11:53:24 +02:00
d4f9726f90 Fix ExtractPhonenumberFromPattern to retain original subject in SearchExtractionResult
- Updated logic in `ExtractPhonenumberFromPattern` to ensure the original subject is preserved in the resulting extraction.
- Adjusted test cases in `ExtractPhonenumberFromPatternTest` to reflect the updated behavior.
2025-10-07 11:40:14 +02:00
8740025dbd Handle case where form is null in PersonIdentifiersDataMapper
- Added a null check for forms in `PersonIdentifiersDataMapper` to prevent potential errors: a form is not present at all steps (on creation / on edit)
- Skips processing if the form is not found.
2025-10-07 11:40:01 +02:00
6d8ef035ea Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-10-07 11:28:04 +02:00
60eab628ee Clarify documentation in PersonIdentifierManagerInterface and fix formatting in SQL query
- Updated PHPDoc in `getWorkers` method to explicitly state that only active definitions are returned.
- Standardized number formatting in SQL query for better readability and consistency.
2025-10-07 11:24:17 +02:00
1fd559b722 Refactor validation of PersonIdentifier 2025-10-07 09:59:52 +02:00
b526e802d7 Add validation logic and tests for StringIdentifier
- Implemented `validate` method in `StringIdentifier` to enforce `only_numbers` and `fixed_length` constraints.
- Created `StringIdentifierValidationTest` to cover validation rules.
2025-10-06 15:16:20 +02:00
60937152c3 Add validate method to PersonIdentifierEngineInterface and related classes
- Introduced `validate` method in `PersonIdentifierEngineInterface`.
- Added `ValidIdentifierConstraint` to `PersonIdentifier` entity.
- Updated `PersonIdentifierWorker` to implement the new `validate` method.
2025-10-06 15:16:12 +02:00
e566f60a4a Apply minor UI update to PersonChooseModal.vue create button
- Added `btn btn-submit` CSS classes to the create button for improved styling.
- Removed outdated and commented-out `on-the-fly` implementation.
2025-10-01 10:51:58 +02:00
c06531cddb Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-09-30 16:01:33 +02:00
4a1da25fee Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-09-30 10:08:56 +02:00
02783e5391 Merge branch 'ticket-app-master' into ticket/64-identifiants-person 2025-09-29 13:21:06 +02:00
be3b9f0f56 Fix issue in PersonIdentifierDataMapper: find the PersonIdentifier against his definition 2025-09-26 15:03:02 +02:00
ee006f55d6 Add unique constraint for definition_id and person_id in PersonIdentifier
- Update entity to include the new database-level unique constraint.
- Add migration script to apply the unique constraint and handle rollback.
2025-09-26 14:44:31 +02:00
13b1c45271 Simplify and modernize entity components and translations for better performance and consistency
- Replace fragmented name rendering with unified `person.text` in Vue components.
- Migrate `GenderIconRenderBox` to use Bootstrap icons and TypeScript.
- Introduce `GenderTranslation` type and helper for gender rendering.
- Refactor `PersonRenderBox` to streamline rendering logic and improve maintainability. Migrate to typescript
- Update French translations for consistency with new gender rendering.
2025-09-26 14:25:38 +02:00
ad2b6d63ac Handle identifier uniqueness validation for same Person in UniqueIdentifierConstraintValidator
- Add logic to skip validation errors for duplicate identifiers belonging to the same Person.
- Update test case to verify no violation is raised for such duplicates.
- Refactor repository query logic to support the new validation scenario.
2025-09-26 13:55:40 +02:00
bfbde078b7 Add personId serialization to PersonJsonNormalizer
- Inject `PersonIdRenderingInterface` into `PersonJsonNormalizer` for generating `personId`.
- Update `PersonJsonNormalizer` to include `personId` in serialized output.
- Extend TypeScript definitions to support `personId` property.
- Enhance unit tests to cover `personId` serialization.
2025-09-25 15:00:11 +02:00
d42a1296c4 Add integration and unit tests for PersonJsonNormalizer to verify normalization behavior
- Introduce `PersonJsonNormalizerIntegrationTest` to test database-driven normalization scenarios.
- Expand `PersonJsonNormalizerTest` with cases covering minimal group normalization and extended keys.
- Refactor test setup to use mock objects and improve coverage of normalization logic.
2025-09-25 14:51:39 +02:00
4b7e3c1601 Restrict deletion of identifier_definition to prevent errors and ensure data integrity
- Updated foreign key constraint on `chill_person_identifier.definition_id` to use `ON DELETE RESTRICT` instead of `ON DELETE CASCADE`.
- Adjusted migration script to handle both the upgrade and downgrade paths for the new restriction.
2025-09-24 12:40:37 +02:00
6ea9af588b Replace value with canonical in PersonIdentifier unique constraint, repository logic, and tests
- Update unique constraint on `PersonIdentifier` to use `canonical` instead of `value`.
- Refactor repository method `findByDefinitionAndValue` to `findByDefinitionAndCanonical`, updating logic accordingly.
- Adjust validation logic in `UniqueIdentifierConstraintValidator` to align with the new canonical-based approach.
- Modify related integration and unit tests to support the changes.
- Inject `PersonIdentifierManagerInterface` into the repository to handle canonical value generation.
2025-09-24 12:40:16 +02:00
0fd76d3fa8 Add PersonIdentifierManagerInterface to PersonACLAwareRepository and enhance search logic for identifiers
- Inject `PersonIdentifierManagerInterface` into `PersonACLAwareRepository` for improved identifier handling.
- Update search queries to include logic for filtering and matching `PersonIdentifier` values.
- Modify test cases to support the new dependency and ensure proper coverage.
2025-09-24 00:01:28 +02:00
34af53130b Refactor and enhance ValidationException handling across types and components
- Simplify and extend type definitions in `types.ts` for dynamic and normalized keys.
- Update `ValidationExceptionInterface` to include new methods for filtering violations.
- Refactor `apiMethods.ts` to leverage updated exception types and key parsing.
- Adjust `WritePersonViolationMap` for stricter type definitions.
- Enhance `PersonEdit.vue` to use refined violation methods, improving validation error handling.
2025-09-23 21:26:12 +02:00
a1fd395868 Add unique constraint for PersonIdentifier, implement UniqueIdentifierConstraint with validation logic, and include supporting tests
- Introduce `UniqueIdentifierConstraint` and its validator for ensuring identifier uniqueness.
- Add a database-level unique constraint on `PersonIdentifier` (`definition_id`, `value`).
- Implement repository method to fetch identifiers by definition and value.
- Include integration and unit tests for validation and repository functionality.
- Update `Person` entity with `Assert\Valid` annotation for `identifiers`.
2025-09-23 12:36:30 +02:00
b8a7cbb321 Add identifiers field in CreationPersonType and handle on_create logic in PersonIdentifiersType
- Introduce `identifiers` field to `CreationPersonType` with a dedicated form type.
- Update `PersonIdentifiersType` to support `step` option (`on_create` and `on_edit`).
- Skip certain identifiers in `on_create` step based on presence configuration.
- Adjust Twig template to display `identifiers` conditionally.
2025-09-22 14:03:59 +02:00
6124eb9e34 Fix isEmpty logic in StringIdentifier: Correct boolean comparison for trimmed content. 2025-09-22 14:03:58 +02:00
a5b06de92a Refactor validation handling in PersonEdit.vue: Replace hasValidationError and validationError with hasViolation and violationTitles. Introduce hasViolationWithParameter and violationTitlesWithParameter for enhanced field validation. Update RequiredIdentifierConstraint messages, improve API error mapping, and refine ValidationException structure with violationsList. Add tests and translations for identifier validation. 2025-09-22 14:03:58 +02:00
52404956d2 Trim PersonIdentifier values during denormalization, implement RequiredIdentifierConstraint and validator, and add tests for empty value validation. 2025-09-22 14:03:57 +02:00
4207efd6bf Remove empty PersonIdentifier values during denormalization and add isEmpty logic to PersonIdentifierWorker. Include tests for empty value handling. 2025-09-22 14:03:57 +02:00
840fde4ad4 Filter PersonIdentifierWorker by presence during initialization and update type definitions. Add presence field to PersonIdentifierWorkerNormalizer. 2025-09-22 14:03:56 +02:00
3611ea2518 Refactor PersonIdentifierDefinition: Replace fully qualified \Doctrine\DBAL\Types\Types references with simplified Types aliases. 2025-09-22 14:03:55 +02:00
bbd4292cb9 Enhance PersonEdit form: Add birthdate input with validation, improve field error handling using hasValidationError, refactor birthDate to respect timezone offsets, and update translations for better user feedback. Replace DateTimeCreate with DateTimeWrite across types and components. 2025-09-22 14:03:55 +02:00
54f8c92240 Update DateNormalizer: Add return type hints for denormalize and normalize methods 2025-09-22 14:03:54 +02:00
5330befc8f eslint fixes 2025-09-22 14:03:54 +02:00
c19206be0c Enhance validation in PersonEdit: Introduce hasValidationError and validationError helpers for form inputs. Improve error feedback for fields such as firstName, lastName, gender, and others. Refactor postPerson to handle validation exceptions and map errors to specific fields. Update related methods, styles, and API error type definitions. 2025-09-22 14:03:53 +02:00
5ff374d2fa Refactor validation handling in apiMethods: Introduce strongly-typed ValidationException and ViolationFromMap. Replace generic validation logic with stricter, type-safe mappings. Update makeFetch to handle Symfony validation problems with enhanced error taxonomy. 2025-09-22 14:03:53 +02:00
4a73aaae94 Replace PhonenumberConstraint with MisdPhoneNumberConstraint across entities, deprecate outdated validation logic, and remove unused methods for improved phone number validation. 2025-09-22 14:03:53 +02:00
ff2c567d05 Update default center type fallback in PersonEdit.vue to "center" for consistency. 2025-09-22 14:03:52 +02:00
a734e84f28 Remove unused Person.vue import from types.ts for cleanup and improved code maintainability. 2025-09-22 14:03:51 +02:00
4367ed086e Enhance person creation workflow: Add onPersonCreated event handling in Create, CreateModal, and AddPersons. Update type definitions and integrate event emission for streamlined person management. 2025-09-22 14:03:51 +02:00
3227bfcd3a Remove serializer.yaml configuration, update PersonJsonNormalizer and PersonJsonDenormalizer for improved logic handling, adjust type hints in closures, and rename id to definition_id in PersonIdentifierWorkerNormalizer. 2025-09-22 14:03:50 +02:00
8d29fb260a Add validation and support for identifiers in PersonJsonDenormalizer, enhance altNames handling, and update tests for improved coverage. Adjust PersonIdentifierManager to handle identifier definitions by ID. 2025-09-22 14:03:50 +02:00
bda0743c63 Update test run guidelines to use the symfony command for executing PHPUnit tests 2025-09-22 14:03:49 +02:00
d9b730627f Introduce PersonJsonReadDenormalizer and PersonJsonDenormalizer to separate responsibilities for handling person denormalization. Add corresponding test classes for improved coverage. Refactor PersonJsonNormalizer to remove denormalization logic. 2025-09-22 14:03:49 +02:00
27548ad654 Add support for person identifiers workflow: update PersonEdit component, API methods, and modals for identifier handling during person creation. Adjust related types for improved consistency. 2025-09-22 14:03:48 +02:00
bec7297039 Add an api list of available person identifiers 2025-09-22 14:03:48 +02:00
852523e644 Refactor person management workflow: Introduce SetGender, SetCivility, and SetCenter lightweight interfaces. Replace PersonState with PersonEdit for streamlined type usage. Enhance queryItems logic and API methods for better consistency. Adjust AddPersons modal to handle query input. 2025-09-22 14:03:47 +02:00
c05d0aad47 Refactor person creation workflow: Introduce PersonEdit component and integrate it across Create, Person.vue, and modals for improved modularity. Update type definitions and API methods for consistency. 2025-09-22 14:03:47 +02:00
1c0ed9abc8 Enhance entity creation: Add CreateModal and integrate with AddPersons workflow. 2025-09-22 14:03:42 +02:00
9aed5cc216 Fix type hinting in PickEntity.vue for addNewEntity function 2025-09-22 14:03:25 +02:00
e4fe5bff68 Allow creating new entities directly from AddPersons modal 2025-09-22 14:03:25 +02:00
4c73c4d9d0 Refactor AddPersons modal into a separate PersonChooseModal component for improved modularity and reusability. 2025-09-22 14:03:24 +02:00
320 changed files with 7485 additions and 12737 deletions

View File

@@ -1,8 +0,0 @@
kind: DX
body: 'Changie: add a field for adding a release note tag when creating an entry in changie.'
time: 2026-03-24T15:38:05.320350835+01:00
custom:
IRN: "No"
Issue: ""
MR: ""
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Feature
body: Add a field "externalId" on center, to ease the synchronisation of centers with external tools
time: 2026-03-24T16:40:18.159561269+01:00
custom:
Issue: "507"
MR: "977"
SchemaChange: Add columns or tables

View File

@@ -1,6 +0,0 @@
kind: Major
body: Add a bundle to deal with tickets
time: 2026-03-26T16:20:18.302331043+01:00
custom:
Issue: ""
SchemaChange: Add columns or tables

View File

@@ -1,6 +0,0 @@
## v4.10.0 - 2025-12-09
### Feature
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
### Fixed
* Remove dependency to package @symfony/ux-translator

View File

@@ -1,6 +0,0 @@
## v4.10.1 - 2025-12-11
### Fixed
* Fix missing translation variable in NewLocation component
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
* Fix use of ByActivityNumberAggregator in combination with activity count exports
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser

View File

@@ -1,9 +0,0 @@
## v4.11.0 - 2025-12-17
### Feature
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
### Fixed
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
* Fix translation key/value
Cannot start with % and should be wrapped in "".

View File

@@ -1,16 +0,0 @@
## v4.12.0 - 2026-01-15
### Feature
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
* Increase the delay before removing stale workflow from 90 days to 180 days.
### Fixed
* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items.
* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'.
* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php
* Fix the calculation of budget balance to only take into account resources and charges that are still actual
* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results
* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself
* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.

View File

@@ -1,4 +0,0 @@
## v4.12.1 - 2026-02-01
### Fixed
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer

View File

@@ -1,15 +0,0 @@
## v4.13.0 - 2026-02-23
### Feature
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
### Fixed
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
* Fixed mispelling of address in translations: addresse -> adresse
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
### DX
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes

View File

@@ -1,6 +0,0 @@
## v4.14.0 - 2026-03-09
### Feature
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
### Fixed
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)

View File

@@ -1,5 +0,0 @@
## v4.14.1 - 2026-03-16
### Security
* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context
### DX
* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures

View File

@@ -1,3 +0,0 @@
## v4.14.2 - 2026-03-18
### Fixed
* Fix link inside notification email

View File

@@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}'
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
changeFormat: >-
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if (eq .Custom.IRN "Yes") }}(RN){{ end }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
**Schema Change**: {{ .Custom.SchemaChange }}
{{- end -}}
@@ -30,20 +30,6 @@ custom:
type: int
minInt: 1
- key: MR
label: Merge request number (on chill-bundles repository) (optional)
optional: true
type: int
minInt: 1
- key: IRN
label: Is this interesting for release notes ?
optional: false
type: enum
enumOptions:
- "No"
- "Yes"
body:
# allow multiline messages
block: true
@@ -60,8 +46,6 @@ kinds:
auto: patch
- label: UX
auto: patch
- label: Major
auto: major
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
---
# Select what we should cache between builds
cache:
paths:
@@ -57,17 +58,18 @@ mirror_chill_zimbra_bundle:
rules:
# 1) Allow manual run from GitLab UI, whatever the branch
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_PIPELINE_SOURCE == "web"'
# 2) Auto-run on commits to master or 472-zimbra-connector
# but only if relevant files changed
- if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "472-zimbra-connector"'
changes:
- packages/ChillZimbraBundle/**/*
- .gitlab-ci.yml
- if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "472-zimbra-connector"'
changes:
- packages/ChillZimbraBundle/**/*
- .gitlab-ci.yml
# 3) Otherwise: never run
- when: never
- when: never
before_script:
- apk add --no-cache git git-subtree openssh
@@ -97,12 +99,10 @@ build:
stage: Composer install
image: chill/base-image:8.3-edge
variables:
COMPOSER_MEMORY_LIMIT: 3G
before_script:
- composer config -g cache-dir "$(pwd)/.cache"
script:
- composer install --optimize-autoloader --no-ansi --no-interaction --no-progress
- php bin/console cache:clear
cache:
paths:
- .cache/
@@ -110,15 +110,12 @@ build:
expire_in: 1 day
paths:
- vendor/
- var/
code_style:
stage: Tests
image: chill/base-image:8.3-edge
script:
- php-cs-fixer fix --dry-run -v --show-progress=none
dependencies:
- build
cache:
paths:
- .cache/
@@ -136,8 +133,6 @@ phpstan_tests:
- bin/console cache:clear --env=dev
script:
- composer exec phpstan -- analyze --memory-limit=3G
dependencies:
- build
cache:
paths:
- .cache/
@@ -153,8 +148,6 @@ rector_tests:
- bin/console cache:clear --env=dev
script:
- composer exec rector -- process --dry-run
dependencies:
- build
cache:
paths:
- .cache/
@@ -173,37 +166,10 @@ lint:
script:
- yarn install --ignore-optional
- npx eslint-baseline "src/**/*.{js,ts,vue}"
dependencies:
- build
cache:
paths:
- node_modules/
artifacts:
expire_in: 1 day
paths:
- vendor/
vue_tsc:
stage: Tests
image: node:20-alpine
before_script:
- apk add --no-cache python3 make g++ py3-setuptools
- export PYTHON="$(which python3)"
- export PATH="./node_modules/.bin:$PATH"
script:
- yarn install --ignore-optional
- yarn vue-tsc --noEmit > vue-tsc-report.txt 2>&1 || true
- cat vue-tsc-report.txt
- grep -q "error" vue-tsc-report.txt && exit 2 || exit 0
dependencies:
- build
cache:
paths:
- node_modules/
artifacts:
expire_in: 1 day
paths:
- vue-tsc-report.txt
---
# psalm_tests:
# stage: Tests
# image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
@@ -229,8 +195,6 @@ unit_tests:
- php bin/console doctrine:fixtures:load -n --env=test
script:
- composer exec phpunit -- --colors=never --exclude-group dbIntensive,openstack-integration
dependencies:
- build
artifacts:
expire_in: 1 day
paths:
@@ -244,5 +208,5 @@ release:
script:
- echo "running release_job"
release:
tag_name: "$CI_COMMIT_TAG"
tag_name: '$CI_COMMIT_TAG'
description: "./.changes/$CI_COMMIT_TAG.md"

View File

@@ -234,17 +234,17 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
#### Running Tests
The tests are run from the project's root (not from the bundle's root).
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests).
Tests must be run using the `symfony` command:
```bash
# Run all tests
symfony composer exec phpunit
# Run a specific test file
symfony composer exec phpunit -- path/to/TestFile.php
# Run a specific test method
symfony composer exec phpunit --filter methodName path/to/TestFile.php
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php
```
When writing tests, only test specific files. Do not run all tests or the full

View File

@@ -6,85 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.14.2 - 2026-03-18
### Fixed
* Fix link inside notification email
## v4.14.1 - 2026-03-16
### Security
* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context
### DX
* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures
## v4.14.0 - 2026-03-09
### Feature
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
### Fixed
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)
## v4.13.0 - 2026-02-23
### Feature
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
### Fixed
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
* Fixed mispelling of address in translations: addresse -> adresse
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
### DX
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes
## v4.12.1 - 2026-02-01
### Fixed
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer
## v4.12.0 - 2026-01-15
### Feature
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
* Increase the delay before removing stale workflow from 90 days to 180 days.
### Fixed
* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items.
* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'.
* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php
* Fix the calculation of budget balance to only take into account resources and charges that are still actual
* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results
* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself
* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.
## v4.11.0 - 2025-12-17
### Feature
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
### Fixed
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
* Fix translation key/value
Cannot start with % and should be wrapped in "".
## v4.10.1 - 2025-12-11
### Fixed
* Fix missing translation variable in NewLocation component
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
* Fix use of ByActivityNumberAggregator in combination with activity count exports
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser
## v4.10.0 - 2025-12-09
### Feature
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
### Fixed
* Remove dependency to package @symfony/ux-translator
## v4.9.0 - 2025-12-05
### Feature
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply

View File

@@ -21,7 +21,7 @@
"ext-openssl": "*",
"ext-redis": "*",
"ext-zlib": "*",
"composer-runtime-api": "*",
"chill-project/chill-zimbra-bundle": "@dev",
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
@@ -62,6 +62,7 @@
"symfony/http-client": "^5.4",
"symfony/http-foundation": "^5.4",
"symfony/intl": "^5.4",
"symfony/loco-translation-provider": "^6.0",
"symfony/mailer": "^5.4",
"symfony/messenger": "^5.4",
"symfony/mime": "^5.4",
@@ -83,7 +84,7 @@
"symfony/templating": "^5.4",
"symfony/translation": "^5.4",
"symfony/twig-bundle": "^5.4",
"symfony/ux-translator": "2.31.0",
"symfony/ux-translator": "^2.22",
"symfony/validator": "^5.4",
"symfony/webpack-encore-bundle": "^1.11",
"symfony/workflow": "^5.4",
@@ -98,7 +99,7 @@
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13",
"friendsofphp/php-cs-fixer": "^3.94",
"friendsofphp/php-cs-fixer": "3.65.0",
"jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15",
@@ -113,7 +114,6 @@
"symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4",
"symfony/flex": "^2.4",
"symfony/loco-translation-provider": "^6.0",
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^7.1",
"symfony/runtime": "^5.4",

View File

@@ -38,4 +38,5 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true],
];

View File

@@ -34,9 +34,6 @@ chill_main:
x: '%env(float:ADD_ADDRESS_MAP_CENTER_X)%'
y: '%env(float:ADD_ADDRESS_MAP_CENTER_Y)%'
z: '%env(float:ADD_ADDRESS_MAP_CENTER_Z)%'
homepage:
default_tab: 'MyCustoms'
display_tabs: ['MyCustoms', 'MyNotifications', 'MyAccompanyingCourses', 'MyEvaluations', 'MyTasks', 'MyWorkflows', 'MyTickets']
when@test:
chill_main:

View File

@@ -8,6 +8,5 @@ when@dev: &dev
- 'file'
- 'md5'
- 'sha1'
seed: 1234567890
when@test: *dev

View File

@@ -11,6 +11,3 @@ services:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
when@dev:
services:
ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface: '@Chill\WopiBundle\Service\Wopi\NullProofValidator'

View File

@@ -12,8 +12,6 @@ As Chill relies on the [symfony ](http://symfony.com) framework, reading the fra
- [Messages to users](messages-to-users.md)
- [Pagination](pagination.md)
- [Localisation](localisation.md)
- [Translation directives](translation_directives.md)
- [Translation provider](translation_provider.md)
- [Logging](logging.md)
- [Database migrations](migrations.md)
- [Searching](searching.md)

View File

@@ -1,376 +0,0 @@
# Translation Key Directives
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
## General Principles
1. **Use lowercase snake_case for all keys**
2. **Use dot-separated namespaces**
The dot is used to reflect:
- bundle
- feature
- sub-feature
- key type
3. **Do not use spaces in keys**
4. **Avoid duplicating the same text in multiple places**
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the `common` namespace.**
## Key Structure
We use the following structure:
```
<scope>.<feature>.<sub-feature>.<key-type>
```
Where:
- `<scope>` identifies the bundle or shared context
- `<feature>` identifies the part of the module using the translation
- `<element>` describes the text purpose
- `<subelement>` for a multi-level element (e.g., activity.export.person.count.description)
### Examples of scopes
- `activity` — ChillActivityBundle
- `person` — ChillPersonBundle
- `common` — neutral shared translation values
## Naming Scopes
### 1. Bundle-specific keys
For most things inside a bundle:
```
activity.<feature>.<element>
```
Example:
```
activity.form.save
activity.list.title
activity.entity.type
activity.menu.activities
activity.controller.success_created
```
### 2. Shared UI elements (buttons, labels, generic text)
These belong in the `common` namespace in ChillMainBundle:
```
common.save
common.delete
common.edit
common.filter
common.duration_time
```
## Translation Workflow
Use the following workflow when deciding where a key belongs:
1. **Is this text used in more than one bundle?**
→ Place in `main` or `common`
2. **Is this text generic UI (button, label, pagination, yes/no)?**
→ Place in `common`
3. **Is this text specific to one bundle and one feature?**
→ Place in `<bundle>.feature.<element>`
4. **Is this text related to an entity or value object?**
→ Place in `<bundle>.entity.<entityname>.<field>`
5. **Is this text used in forms?**
`<bundle>.form.<field>` or `<bundle>.form.<action>`
6. **Is this text related to exports?**
`<bundle>.export.<feature>.<column>`
7. **Is it related to filtering, searching or parameters?**
`<bundle>.filter.<name>` or
`<bundle>.filter.<feature>.<field>` for nested filters
## Examples Based on Translations Within ChillActivityBundle
Below are concrete examples from `ChillActivityBundle`, refactored according to the guidelines.
### General activity keys
Instead of scattered keys like:
```
Show the activity
Edit the activity
Activity
Duration time
...
```
We use:
```
activity.general.show
activity.general.edit
activity.general.title
activity.general.duration
activity.general.travel_time
activity.general.attendee
activity.general.remark
activity.general.no_comments
```
### Forms
Instead of keys like:
```
Activity creation
Save activity
Reset form
Choose a type
```
Use:
```
activity.form.title_create
activity.form.save
activity.form.reset
activity.form.choose_type
activity.form.choose_duration
```
Long lists (like durations) should be grouped:
```
activity.form.duration.5min
activity.form.duration.10min
activity.form.duration.15min
activity.form.duration.1h
activity.form.duration.1h30
activity.form.duration.2h
...
```
### Entities
Entity fields should follow:
```
activity.entity.activity.date
activity.entity.activity.comment
activity.entity.activity.deleted
activity.entity.location.name
activity.entity.location.type
```
### Controller messages
Instead of strings as keys:
```
'Success : activity created!'
'The form is not valid. The activity has not been created !'
```
Use:
```
activity.controller.success_created
activity.controller.error_invalid_create
activity.controller.success_updated
activity.controller.error_invalid_update
```
### Roles
Access control keys should be:
```
activity.role.create
activity.role.update
activity.role.see
activity.role.see_details
activity.role.delete
activity.role.stats
activity.role.list
```
### Admin
```
activity.admin.configuration
activity.admin.types
activity.admin.reasons
activity.admin.reason_category
activity.admin.presence
```
### CRUD
```
activity.crud.type.title_new
activity.crud.type.title_edit
activity.crud.presence.title_new
```
### Activity Reason
```
activity.reason.list
activity.reason.create
activity.reason.active
activity.reason.category
activity.reason.entity_title
```
### Exports
Group them logically:
```
activity.export.person.count.title
activity.export.person.count.description
activity.export.person.count.header
activity.export.period.sum_duration.title
activity.export.period.sum_duration.description
activity.export.period.sum_duration.header
```
### Filters
Use hierarchical filters:
```
activity.filter.by_reason
activity.filter.by_type
activity.filter.by_date
activity.filter.by_location
activity.filter.by_sent_received
activity.filter.by_user
```
### Aggregators
```
activity.aggregator.reason.by_category
activity.aggregator.reason.level
activity.aggregator.user.by_scope
activity.aggregator.user.by_job
```
## Global/Shared Keys
Keys like the following **must not be redeclared** in each bundle:
- First name
- Last name
- Username
- ID
- Type
- Duration
- Comment
- Date
- Location
- Present / Not present
- Add / Edit / Delete / Save / Update
These belong in `common` or `main`:
```
common.firstname
common.lastname
common.username
common.id
common.type
common.comment
common.date
common.location
common.present
common.absent
common.add
common.edit
common.delete
common.save
common.update
```
## Naming Directives Summary
- **snake_case**
- **namespaced with dots**
- **bundle prefix for bundle-specific concepts**
- **common or main for shared concepts**
- **avoid free-floating keys (without namespace)**
- **reuse common keys wherever possible**
## Migration Strategy (Optional)
To apply this structure progressively:
1. New keys must follow these guidelines.
2. Existing keys may remain as-is until refactored.
3. When refactoring:
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
- Replace duplicated keys with shared ones.
---
# Avoiding Duplicate Translations
## 1. Use Shared Namespaces
Two namespaces must be used for shared translations:
- `common.*` — generic UI concepts (save, delete, date, name, etc.)
If a translation may be reused in multiple bundles, it must be placed in the `common` namespace or in ChillMainBundle.
## 2. Bundle-Specific Keys
Keys belonging only to one bundle or one feature are namespaced inside that bundle:
```
activity.<feature>.<element>
person.<feature>.<element>
```
## 3. Search Before Creating
Before adding a new translation key, developers must:
1. For common translations like "enregistrer/opslaan" look in the `common` namespace.
2. Search in Loco or translations for existing values.
If a suitable key exists, reuse it.
## 4. Only Create a New Key When Necessary
Create a new key only when the text is:
- specific to the bundle
- specific to the feature
- not reusable elsewhere
## 5. Progressive Cleanup
Old duplicates may remain temporarily. When updating code in an area, clean duplicate values by moving them into `common` or `main`.
## General Workflow
- **Reuse shared keys** within `common` namespace.
- **Search before creating** new keys.
- **Namespace bundle-specific keys** under their bundle.
- **Refactor progressively** when touching old code.

View File

@@ -0,0 +1,419 @@
============================================
Directives for creating new translation keys
============================================
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
General Principles
==================
1. **Use lowercase snake_case for all keys**
2. **Use dot-separated namespaces**
The dot is used to reflect:
- bundle
- feature
- sub-feature
- key type
3. **Do not use spaces in keys**
4. **Avoid duplicating the same text in multiple places**
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the ``common`` namespace.**
Key Structure
=============
We use the following structure:
.. code-block:: text
<scope>.<feature>.<sub-feature>.<key-type>
Where:
* ``<>`` identifies the bundle or shared context
* ``<feature>`` identifies the part of the module using the translation
* ``<element>`` describes the text purpose
* ``<subelement>`` for a multi-level element ( eg. activity.export.person.count.description)
Examples of scopes
------------------
* ``activity`` — ChillActivityBundle
* ``person`` — ChillPersonBundle
* ``common`` — neutral shared translation values
Naming Scopes
=============
1. **Bundle-specific keys**
For most things inside a bundle:
.. code-block:: text
activity.<feature>.<element>
Example:
.. code-block:: text
activity.form.save
activity.list.title
activity.entity.type
activity.menu.activities
activity.controller.success_created
2. **Shared UI elements (buttons, labels, generic text)**
These belong in the ``common`` namespace in ChillMainBundle:
.. code-block:: text
common.save
common.delete
common.edit
common.filter
common.duration_time
Translation workflow
====================
Use the following workflow when deciding where a key belongs:
1. **Is this text used in more than one bundle?**
→ Place in ``main`` or ``common``
2. **Is this text generic UI (button, label, pagination, yes/no)?**
→ Place in ``common``
3. **Is this text specific to one bundle and one feature?**
→ Place in ``<bundle>.feature.<element>``
4. **Is this text related to an entity or value object?**
→ Place in ``<bundle>.entity.<entityname>.<field>``
5. **Is this text used in forms?**
``<bundle>.form.<field>`` or ``<bundle>.form.<action>``
6. **Is this text related to exports?**
``<bundle>.export.<feature>.<column>``
7. **Is it related to filtering, searching or parameters?**
``<bundle>.filter.<name>`` or
``<bundle>.filter.<feature>.<field>`` for nested filters
Examples based on translations within ChillActivityBundle
=========================================================
Below are concrete examples from ``ChillActivityBundle``,
refactored according to the guidelines.
General activity keys
---------------------
Instead of scattered keys like::
Show the activity
Edit the activity
Activity
Duration time
...
We use:
.. code-block:: text
activity.general.show
activity.general.edit
activity.general.title
activity.general.duration
activity.general.travel_time
activity.general.attendee
activity.general.remark
activity.general.no_comments
Forms
-----
Instead of keys like::
Activity creation
Save activity
Reset form
Choose a type
Use:
.. code-block:: text
activity.form.title_create
activity.form.save
activity.form.reset
activity.form.choose_type
activity.form.choose_duration
Long lists (like durations) should be grouped:
.. code-block:: text
activity.form.duration.5min
activity.form.duration.10min
activity.form.duration.15min
activity.form.duration.1h
activity.form.duration.1h30
activity.form.duration.2h
...
Entities
--------
Entity fields should follow:
.. code-block:: text
activity.entity.activity.date
activity.entity.activity.comment
activity.entity.activity.deleted
activity.entity.location.name
activity.entity.location.type
Controller messages
-------------------
Instead of strings as keys::
'Success : activity created!'
'The form is not valid. The activity has not been created !'
Use:
.. code-block:: text
activity.controller.success_created
activity.controller.error_invalid_create
activity.controller.success_updated
activity.controller.error_invalid_update
Roles
-----
Access control keys should be:
.. code-block:: text
activity.role.create
activity.role.update
activity.role.see
activity.role.see_details
activity.role.delete
activity.role.stats
activity.role.list
Admin
-----
.. code-block:: text
activity.admin.configuration
activity.admin.types
activity.admin.reasons
activity.admin.reason_category
activity.admin.presence
CRUD
----
.. code-block:: text
activity.crud.type.title_new
activity.crud.type.title_edit
activity.crud.presence.title_new
Activity Reason
---------------
.. code-block:: text
activity.reason.list
activity.reason.create
activity.reason.active
activity.reason.category
activity.reason.entity_title
Exports
-------
Group them logically:
.. code-block:: text
activity.export.person.count.title
activity.export.person.count.description
activity.export.person.count.header
activity.export.period.sum_duration.title
activity.export.period.sum_duration.description
activity.export.period.sum_duration.header
Filters
-------
Use hierarchical filters:
.. code-block:: text
activity.filter.by_reason
activity.filter.by_type
activity.filter.by_date
activity.filter.by_location
activity.filter.by_sent_received
activity.filter.by_user
Aggregators
-----------
.. code-block:: text
activity.aggregator.reason.by_category
activity.aggregator.reason.level
activity.aggregator.user.by_scope
activity.aggregator.user.by_job
Global/Shared Keys
==================
Keys like the following **must not be redeclared** in each bundle:
- First name
- Last name
- Username
- ID
- Type
- Duration
- Comment
- Date
- Location
- Present / Not present
- Add / Edit / Delete / Save / Update
These belong in ``common`` or ``main``:
.. code-block:: text
common.firstname
common.lastname
common.username
common.id
common.type
common.comment
common.date
common.location
common.present
common.absent
common.add
common.edit
common.delete
common.save
common.update
Naming directives summary
==========================
* **snake_case**
* **namespaced with dots**
* **bundle prefix for bundle-specific concepts**
* **common or main for shared concepts**
* **avoid free-floating keys (without namespace)**
* **reuse common keys wherever possible**
Migration Strategy (Optional)
=============================
To apply this structure progressively:
1. New keys must follow these guidelines.
2. Existing keys may remain as-is until refactored.
3. When refactoring:
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
- Replace duplicated keys with shared ones.
===========================================
Avoiding duplicate translations
===========================================
1. Use Shared Namespaces
========================
Two namespaces must be used for shared translations:
* ``common.*`` — generic UI concepts (save, delete, date, name, etc.)
If a translation may be reused in multiple bundles, it must be placed
in the ``common`` namespace or in ChillMainBundle.
2. Bundle-Specific Keys
=======================
Keys belonging only to one bundle or one feature are namespaced inside that
bundle:
.. code-block:: text
activity.<feature>.<element>
person.<feature>.<element>
3. Search Before Creating
=========================
Before adding a new translation key, developers must:
1. For common translations like: "enregistrer/opslaan" look in the `common` namespace.
3. Search in Loco or translations for existing values.
If a suitable key exists, reuse it.
4. Only Create a New Key When Necessary
=======================================
Create a new key only when the text is:
* specific to the bundle
* specific to the feature
* not reusable elsewhere
6. Progressive Cleanup
======================
Old duplicates may remain temporarily. When updating code in an area, clean
duplicate values by moving them into ``common`` or ``main``.
General workflow
================
* **Reuse shared keys** within ``common`` namespace.
* **Search before creating** new keys.
* **Namespace bundle-specific keys** under their bundle.
* **Refactor progressively** when touching old code.

View File

@@ -1,139 +0,0 @@
# Managing Translations Within CHILL Using Loco as a Translation Provider
Within CHILL we make use of Symfony's translation component together with *Loco* as an external translation provider. Using this setup centralises translations in a single online location (Loco), while still allowing developers to create and update translation keys locally in the project (YAML files).
## Workflow
We use the following workflow:
- Developers create translation keys in YAML files inside each bundle.
- Keys are written in **English**.
- Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
- Loco acts as the central translation memory and synchronisation source.
- Loco Symfony package was installed so that built-in translation commands can be used to push/pull content between Loco and the local project.
## Translation Directory Structure
Each bundle contains its own `translations` directory, for example:
```
chill-bundles/
ChillCoreBundle/
translations/
messages.fr.yml
messages.nl.yml
ChillPersonBundle/
translations/
messages.fr.yml
messages.nl.yml
...
```
## Configuration
The translation configuration is defined in `config/packages/translation.yaml`:
```yaml
framework:
default_locale: '%env(resolve:LOCALE)%'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- '%env(resolve:LOCALE)%'
- 'en'
providers:
loco:
dsn: '%env(LOCO_DSN)%'
domains: [ 'messages' ]
locales: [ 'fr', 'nl' ]
```
Note:
- `en` is the **source locale** in Loco.
- `fr` and `nl` are the **application locales**.
- `domains: [messages]` means only `messages.*.yml` files are pushed.
### Environment Variables
In `.env`:
```
LOCALE=fr
```
In `.env.local`:
```
LOCO_DSN="loco://API_KEY@default"
```
Replace `API_KEY` with the key provided by Loco.
## Working with Loco
Loco shows all translation keys under three languages:
- **English (source)** — keys are listed but remain "untranslated"
- **French** — translated strings for French users
- **Dutch** — translated strings for Dutch users
Note: Don't add translations directly in the English column. This column simply represents the *key*.
## Pushing Translations to Loco
You can push local translations to Loco using:
```bash
symfony console translation:push loco --locales=fr --locales=nl --force
```
This will:
- Upload all French and Dutch translation values from `*.fr.yml` and `*.nl.yml` files
- Ensure Loco stays in sync with local YAML files
- Create any missing keys in Loco
## Pulling Translations from Loco
When translators update strings in Loco, developers can fetch updates with:
```bash
symfony console translation:pull loco --locales=fr --locales=nl --force
```
This will:
- Download the latest French and Dutch translations
- Overwrite the local YAML files with Loco's content
- Keep everything consistent across the team
## Adding New Translation Keys (Developer Workflow)
1. Add a new key directly in the appropriate YAML file, for example:
```
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
```
Example key:
```yaml
person.form.submit: "Envoyer"
```
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later):
```yaml
person.form.submit: "Verzenden"
```
3. Run a push to send the new key to Loco:
```bash
symfony console translation:push loco --locales=fr --locales=nl --force
```
4. The key will now appear in Loco for translation management.
Note: English appears as "untranslated", because it is merely the source language.

View File

@@ -0,0 +1,148 @@
=======================================================================
Managing translations within CHILL using Loco as a translation provider
=======================================================================
Within CHILL we make use of Symfony's translation component together with *Loco* as an external
translation provider. Using this setup centralise translations in a single online
location (Loco), while still allowing developers to create and update
translation keys locally in the project (YAML files).
Workflow
========
We use the following workflow:
* Developers create translation keys in YAML files inside each bundle.
* Keys are written in **English**.
* Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
* Loco acts as the central translation memory and synchronisation source.
* Loco Symfony package was installed so that built-in translation commands can be used to push/pull content
between Loco and the local project.
Translation directory structure
===============================
Each bundle contains its own ``translations`` directory, for example::
chill-bundles/
ChillCoreBundle/
translations/
messages.fr.yml
messages.nl.yml
ChillPersonBundle/
translations/
messages.fr.yml
messages.nl.yml
...
Configuration
=============
The translation configuration is defined in
``config/packages/translation.yaml``::
framework:
default_locale: '%env(resolve:LOCALE)%'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- '%env(resolve:LOCALE)%'
- 'en'
providers:
loco:
dsn: '%env(LOCO_DSN)%'
domains: [ 'messages' ]
locales: [ 'fr', 'nl' ]
Note:
* ``en`` is the **source locale** in Loco.
* ``fr`` and ``nl`` are the **application locales**.
* ``domains: [messages]`` means only ``messages.*.yml`` files are pushed.
Environment variables
---------------------
In ``.env``::
LOCALE=fr
In ``.env.local``::
LOCO_DSN="loco://API_KEY@default"
Replace ``API_KEY`` with the key provided by Loco.
Working with Loco
=================
Loco shows all translation keys under three languages:
* **English (source)** — keys are listed but remain “untranslated”
* **French** — translated strings for French users
* **Dutch** — translated strings for Dutch users
Note: Don't add translations directly in the English column.
This column simply represents the *key*.
Pushing translations to Loco
============================
You can push local translations to Loco using:
.. code-block:: bash
symfony console translation:push loco --locales=fr --locales=nl --force
This will:
* Upload all French and Dutch translation values from ``*.fr.yml`` and
``*.nl.yml`` files
* Ensures Loco stays in sync with local YAML files
* Creates any missing keys in Loco
Pulling translations from Loco
==============================
When translators update strings in Loco, developers can fetch updates with:
.. code-block:: bash
symfony console translation:pull loco --locales=fr --locales=nl --force
This will:
* Download the latest French and Dutch translations
* Overwrite the local YAML files with Locos content
* Keep everything consistent across the team
Adding new translation keys (Developer workflow)
================================================
1. Add a new key directly in the appropriate YAML file, for example::
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
Example key::
person.form.submit: "Envoyer"
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later)::
person.form.submit: "Verzenden"
3. Run a push to send the new key to Loco:
.. code-block:: bash
symfony console translation:push loco --locales=fr --locales=nl --force
4. The key will now appear in Loco for translation management.
Note: English appears as “untranslated”, because it is merely the source language

View File

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

View File

@@ -1,3 +0,0 @@
kind: Added
body: Use admin delegated account for handling authentication
time: 2026-01-22T15:32:23.932994899+01:00

View File

@@ -9,7 +9,7 @@
"social worker"
],
"require": {
"chill-project/chill-bundles": "^4.9.0",
"chill-project/chill-bundles": "dev-master as v4.6.1",
"zimbra-api/soap-api": "^3.2.2",
"psr/http-client": "^1.0",
"nyholm/psr7": "^1.0"

View File

@@ -80,19 +80,12 @@ final readonly class CreateZimbraComponent
$location = $calendar->getCalendar()->getLocation();
$hasLocation = $calendar->getCalendar()->hasLocation();
$isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false;
} elseif ($calendar instanceof Calendar) {
} else {
$startDate = $calendar->getStartDate();
$endDate = $calendar->getEndDate();
$location = $calendar->getLocation();
$hasLocation = $calendar->hasLocation();
$isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false;
} else {
// Calendar range case
$startDate = $calendar->getStartDate();
$endDate = $calendar->getEndDate();
$location = $calendar->getLocation();
$hasLocation = $calendar->hasLocation();
$isPrivate = false;
}
$comp = new InviteComponent();

View File

@@ -11,84 +11,48 @@ declare(strict_types=1);
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Zimbra\Admin\AdminApi;
use Zimbra\Common\Enum\AccountBy;
use Zimbra\Common\Soap\ClientFactory;
use Zimbra\Common\Struct\AccountSelector;
use Zimbra\Common\Struct\Header\AccountInfo;
use Zimbra\Mail\MailApi;
final class SoapClientBuilder
final readonly class SoapClientBuilder
{
private readonly string $username;
private string $username;
private readonly string $password;
private string $password;
private readonly string $url;
private string $url;
private readonly string $adminUrl;
private readonly bool $verifyHost;
private readonly bool $verifyPeer;
private readonly bool $adminVerifyHost;
private readonly bool $adminVerifyPeer;
/**
* Keep the cache of the tokens.
*
* @var array<string, array{token: string, expirationTime: \DateTimeImmutable}>
*/
private array $tokenCache = [];
public function __construct(
private readonly ParameterBagInterface $parameterBag,
private readonly HttpClientInterface $client,
private readonly ClockInterface $clock,
) {
public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client)
{
$dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
$url = parse_url($dsn);
$this->username = urldecode($url['user']);
$this->password = urldecode($url['pass']);
if ('zimbra+http' === $url['scheme']) {
$scheme = 'http';
$scheme = 'http://';
$port = $url['port'] ?? 80;
} elseif ('zimbra+https' === $url['scheme']) {
$scheme = 'https';
$scheme = 'https://';
$port = $url['port'] ?? 443;
} else {
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']);
}
// get attributes for adminUrl
$query = [];
parse_str($url['query'] ?? '', $query);
$adminPort = $query['adminPort'] ?? '7071';
$adminHost = $query['adminHost'] ?? $url['host'];
$adminScheme = $query['adminScheme'] ?? $scheme;
$this->verifyPeer = (bool) ($query['verifyPeer'] ?? true);
$this->verifyHost = (bool) ($query['verifyHost'] ?? true);
$this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer);
$this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost);
$this->url = $scheme.'://'.$url['host'].':'.$port;
$this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort;
$this->url = $scheme.$url['host'].':'.$port;
}
private function buildApi(): MailApi
{
$baseClient = $this->client->withOptions([
'base_uri' => $location = $this->url.'/service/soap',
'verify_host' => $this->verifyHost,
'verify_peer' => $this->verifyPeer,
'verify_host' => false,
'verify_peer' => false,
]);
$psr18Client = new Psr18Client($baseClient);
$api = new MailApi();
@@ -98,36 +62,12 @@ final class SoapClientBuilder
return $api;
}
private function buildAdminApi(): AdminApi
{
$baseClient = $this->client->withOptions([
'base_uri' => $location = $this->adminUrl.'/service/admin/soap',
'verify_host' => $this->adminVerifyHost,
'verify_peer' => $this->adminVerifyPeer,
]);
$psr18Client = new Psr18Client($baseClient);
$api = new AdminApi();
$client = ClientFactory::create($location, $psr18Client);
$api->setClient($client);
return $api;
}
public function getApiForAccount(string $accountName): MailApi
{
['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName]
?? ['token' => null, 'expirationTime' => null];
$api = $this->buildApi();
$response = $api->authByAccountName($this->username, $this->password);
if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) {
$adminApi = $this->buildAdminApi();
$adminApi->auth($this->username, $this->password);
$delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName));
$token = $delegateResponse->getAuthToken();
$expiration = $delegateResponse->getLifetime();
$expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S'));
$this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime];
}
$token = $response->getAuthToken();
$apiBy = $this->buildApi();
$apiBy->setAuthToken($token);

View File

@@ -3,6 +3,7 @@ parameters:
paths:
- src/
- utils/
- packages/
tmpDir: var/cache/phpstan
reportUnmatchedIgnoredErrors: false
excludePaths:

View File

@@ -33,7 +33,6 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
public function __construct(private readonly EntityManagerInterface $em)
{
mt_srand(123456789);
$this->faker = FakerFactory::create('fr_FR');
}
@@ -49,7 +48,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
->findAll();
foreach ($persons as $person) {
$activityNbr = mt_rand(0, 3);
$activityNbr = random_int(0, 3);
for ($i = 0; $i < $activityNbr; ++$i) {
$activity = $this->newRandomActivity($person);
@@ -74,7 +73,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
// ->setAttendee($this->faker->boolean())
for ($i = 0; mt_rand(0, 4) > $i; ++$i) {
for ($i = 0; random_int(0, 4) > $i; ++$i) {
$reason = $this->getRandomActivityReason();
if (null !== $reason) {

View File

@@ -69,7 +69,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
}
/** (non-PHPdoc).
* @see PrependExtensionInterface::prepend()
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prependRoutes(ContainerBuilder $container)
{

View File

@@ -27,8 +27,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
{
$qb
// Use a distinct alias inside the subquery to avoid colliding with the root alias "activity"
->addSelect('(SELECT COUNT(agg_activity.id) FROM '.Activity::class.' agg_activity WHERE agg_activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
->addSelect('(SELECT COUNT(activity.id) FROM '.Activity::class.' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
->addGroupBy('activity_by_number_aggregator');
}
@@ -66,7 +65,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
{
return static function ($value) {
if ('_header' === $value) {
return 'Count activities linked to an accompanying period';
return '';
}
if (null === $value) {

View File

@@ -24,7 +24,6 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
@@ -341,7 +340,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
}
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $participation->getAccompanyingPeriod())) {
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
continue;
}

View File

@@ -127,8 +127,6 @@ import {
ACTIVITY_LOCATION_FIELDS_TYPE,
ACTIVITY_CHOOSE_LOCATION_TYPE,
ACTIVITY_CREATE_NEW_LOCATION,
ACTIVITY_EDIT_ADDRESS,
ACTIVITY_CREATE_ADDRESS,
trans,
} from "translator";
@@ -149,8 +147,6 @@ export default {
ACTIVITY_LOCATION_FIELDS_TYPE,
ACTIVITY_CHOOSE_LOCATION_TYPE,
ACTIVITY_CREATE_NEW_LOCATION,
ACTIVITY_EDIT_ADDRESS,
ACTIVITY_CREATE_ADDRESS,
};
},
props: ["availableLocations"],
@@ -174,14 +170,14 @@ export default {
options: {
button: {
text: {
create: ACTIVITY_CREATE_ADDRESS,
edit: ACTIVITY_EDIT_ADDRESS,
create: "activity.create_address",
edit: "activity.edit_address",
},
size: "btn-sm",
},
title: {
create: ACTIVITY_CREATE_ADDRESS,
edit: ACTIVITY_EDIT_ADDRESS,
create: "activity.create_address",
edit: "activity.edit_address",
},
},
context: {

View File

@@ -1,98 +1,103 @@
<template>
<teleport to="#social-issues-acc">
<div class="mb-3 row">
<div class="col-4">
<label :class="socialIssuesClassList">{{
trans(ACTIVITY_SOCIAL_ISSUES)
}}</label>
</div>
<div class="col-8">
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected"
>
</check-social-issue>
<teleport to="#social-issues-acc">
<div class="mb-3 row">
<div class="col-4">
<label :class="socialIssuesClassList">{{
trans(ACTIVITY_SOCIAL_ISSUES)
}}</label>
</div>
<div class="col-8">
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected"
>
</check-social-issue>
<div class="my-3">
<VueMultiselect
name="otherIssues"
label="text"
track-by="id"
open-direction="bottom"
:close-on-select="true"
:preserve-search="false"
:reset-after="true"
:hide-selected="true"
:taggable="false"
:multiple="false"
:searchable="true"
:allow-empty="true"
:show-labels="false"
:loading="issueIsLoading"
:placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)"
:options="socialIssuesOther"
@select="addIssueInList"
>
</VueMultiselect>
</div>
</div>
</div>
<div class="mb-3 row">
<div class="col-4">
<label :class="socialActionsClassList">{{
trans(ACTIVITY_SOCIAL_ACTIONS)
}}</label>
</div>
<div class="col-8">
<div v-if="actionIsLoading === true">
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i>
<div class="my-3">
<VueMultiselect
name="otherIssues"
label="text"
track-by="id"
open-direction="bottom"
:close-on-select="true"
:preserve-search="false"
:reset-after="true"
:hide-selected="true"
:taggable="false"
:multiple="false"
:searchable="true"
:allow-empty="true"
:show-labels="false"
:loading="issueIsLoading"
:placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)"
:options="socialIssuesOther"
@select="addIssueInList"
>
</VueMultiselect>
</div>
</div>
</div>
<span
v-else-if="socialIssuesSelected.length === 0"
class="inline-choice chill-no-data-statement mt-3"
>
{{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }}
</span>
<div class="mb-3 row">
<div class="col-4">
<label :class="socialActionsClassList">{{
trans(ACTIVITY_SOCIAL_ACTIONS)
}}</label>
</div>
<div class="col-8">
<div v-if="actionIsLoading === true">
<i
class="chill-green fa fa-circle-o-notch fa-spin fa-lg"
></i>
</div>
<template
v-else-if="
socialActionsList.length > 0 &&
(socialIssuesSelected.length || socialActionsSelected.length)
"
>
<div
id="actionsList"
v-for="group in socialActionsList"
:key="group.issue"
>
<span class="badge bg-chill-l-gray text-dark">{{
group.issue
}}</span>
<check-social-action
v-for="action in group.actions"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected"
>
</check-social-action>
</div>
</template>
<span
v-else-if="socialIssuesSelected.length === 0"
class="inline-choice chill-no-data-statement mt-3"
>
{{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }}
</span>
<span
v-else-if="actionAreLoaded && socialActionsList.length === 0"
class="inline-choice chill-no-data-statement mt-3"
>
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
</span>
</div>
</div>
</teleport>
<template
v-else-if="
socialActionsList.length > 0 &&
(socialIssuesSelected.length ||
socialActionsSelected.length)
"
>
<div
id="actionsList"
v-for="group in socialActionsList"
:key="group.issue"
>
<span class="badge bg-chill-l-gray text-dark">{{
group.issue
}}</span>
<check-social-action
v-for="action in group.actions"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected"
>
</check-social-action>
</div>
</template>
<span
v-else-if="
actionAreLoaded && socialActionsList.length === 0
"
class="inline-choice chill-no-data-statement mt-3"
>
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
</span>
</div>
</div>
</teleport>
</template>
<script>
@@ -101,174 +106,175 @@ import CheckSocialIssue from "./SocialIssuesAcc/CheckSocialIssue.vue";
import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue";
import { getSocialIssues, getSocialActionByIssue } from "../api.js";
import {
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
trans,
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
trans,
} from "translator";
export default {
name: "SocialIssuesAcc",
components: {
CheckSocialIssue,
CheckSocialAction,
VueMultiselect,
},
setup() {
return {
trans,
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
};
},
data() {
return {
issueIsLoading: false,
actionIsLoading: false,
actionAreLoaded: false,
socialIssuesClassList: {
"col-form-label": true,
required: false,
},
socialActionsClassList: {
"col-form-label": true,
required: false,
},
};
},
computed: {
socialIssuesList() {
return this.$store.state.activity.accompanyingPeriod.socialIssues;
name: "SocialIssuesAcc",
components: {
CheckSocialIssue,
CheckSocialAction,
VueMultiselect,
},
socialIssuesSelected() {
return this.$store.state.activity.socialIssues;
setup() {
return {
trans,
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
};
},
socialIssuesOther() {
return this.$store.state.socialIssuesOther;
data() {
return {
issueIsLoading: false,
actionIsLoading: false,
actionAreLoaded: false,
socialIssuesClassList: {
"col-form-label": true,
required: false,
},
socialActionsClassList: {
"col-form-label": true,
required: false,
},
};
},
socialActionsList() {
return this.$store.getters.socialActionsListSorted;
computed: {
socialIssuesList() {
return this.$store.state.activity.accompanyingPeriod.socialIssues;
},
socialIssuesSelected() {
return this.$store.state.activity.socialIssues;
},
socialIssuesOther() {
return this.$store.state.socialIssuesOther;
},
socialActionsList() {
return this.$store.getters.socialActionsListSorted;
},
socialActionsSelected() {
return this.$store.state.activity.socialActions;
},
},
socialActionsSelected() {
return this.$store.state.activity.socialActions;
},
},
mounted() {
/* Load classNames after element is present */
const socialActionsEl = document.querySelector(
"input#chill_activitybundle_activity_socialActions",
);
if (socialActionsEl && socialActionsEl.hasAttribute("required")) {
this.socialActionsClassList.required = true;
}
const socialIssuesEl = document.querySelector(
"input#chill_activitybundle_activity_socialIssues",
);
if (socialIssuesEl && socialIssuesEl.hasAttribute("required")) {
this.socialIssuesClassList.required = true;
}
/* Load other issues in multiselect */
this.issueIsLoading = true;
this.actionAreLoaded = false;
getSocialIssues().then((response) => {
/* Add issues to the store */
this.$store.commit("updateIssuesOther", response);
/* Add in list the issues already associated (if not yet listed) */
this.socialIssuesSelected.forEach((issue) => {
if (
this.socialIssuesList.filter((i) => i.id === issue.id).length !== 1
) {
this.$store.commit("addIssueInList", issue);
mounted() {
/* Load classNames after element is present */
const socialActionsEl = document.querySelector(
"input#chill_activitybundle_activity_socialActions",
);
if (socialActionsEl && socialActionsEl.hasAttribute("required")) {
this.socialActionsClassList.required = true;
}
});
/* Remove from multiselect the issues that are not yet in the checkbox list */
this.socialIssuesList.forEach((issue) => {
this.$store.commit("removeIssueInOther", issue);
});
const socialIssuesEl = document.querySelector(
"input#chill_activitybundle_activity_socialIssues",
);
if (socialIssuesEl && socialIssuesEl.hasAttribute("required")) {
this.socialIssuesClassList.required = true;
}
/* Filter issues */
this.$store.commit("filterList", "issues");
/* Load other issues in multiselect */
this.issueIsLoading = true;
this.actionAreLoaded = false;
/* Add in list the actions already associated (if not yet listed) */
this.socialActionsSelected.forEach((action) => {
this.$store.commit("addActionInList", action);
});
getSocialIssues().then((response) => {
/* Add issues to the store */
this.$store.commit("updateIssuesOther", response);
/* Filter actions */
this.$store.commit("filterList", "actions");
/* Add in list the issues already associated (if not yet listed) */
this.socialIssuesSelected.forEach((issue) => {
if (
this.socialIssuesList.filter((i) => i.id === issue.id)
.length !== 1
) {
this.$store.commit("addIssueInList", issue);
}
});
this.issueIsLoading = false;
this.actionAreLoaded = true;
this.updateActionsList();
});
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected),
/* Remove from multiselect the issues that are not yet in the checkbox list */
this.socialIssuesList.forEach((issue) => {
this.$store.commit("removeIssueInOther", issue);
});
/* Filter issues */
this.$store.commit("filterList", "issues");
/* Add in list the actions already associated (if not yet listed) */
this.socialActionsSelected.forEach((action) => {
this.$store.commit("addActionInList", action);
});
/* Filter actions */
this.$store.commit("filterList", "actions");
this.issueIsLoading = false;
this.actionAreLoaded = true;
this.updateActionsList();
});
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected),
remove it from multiselect, and add socialActions concerned
*/
addIssueInList(value) {
//console.log('addIssueInList', value);
this.$store.commit("addIssueInList", value);
this.$store.commit("removeIssueInOther", value);
this.$store.dispatch("addIssueSelected", value);
this.updateActionsList();
},
/* Update value for selected issues checkboxes
*/
updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues);
this.$store.dispatch("updateIssuesSelected", issues);
this.updateActionsList();
},
/* Update value for selected actions checkboxes
*/
updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions);
this.$store.dispatch("updateActionsSelected", actions);
},
/* Add socialActions concerned: after reset, loop on each issue selected
addIssueInList(value) {
//console.log('addIssueInList', value);
this.$store.commit("addIssueInList", value);
this.$store.commit("removeIssueInOther", value);
this.$store.dispatch("addIssueSelected", value);
this.updateActionsList();
},
/* Update value for selected issues checkboxes
*/
updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues);
this.$store.dispatch("updateIssuesSelected", issues);
this.updateActionsList();
},
/* Update value for selected actions checkboxes
*/
updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions);
this.$store.dispatch("updateActionsSelected", actions);
},
/* Add socialActions concerned: after reset, loop on each issue selected
to get social actions concerned
*/
updateActionsList() {
this.resetActionsList();
this.socialIssuesSelected.forEach((item) => {
this.actionIsLoading = true;
getSocialActionByIssue(item.id).then(
(actions) =>
new Promise((resolve) => {
actions.results.forEach((action) => {
this.$store.commit("addActionInList", action);
}, this);
updateActionsList() {
this.resetActionsList();
this.socialIssuesSelected.forEach((item) => {
this.actionIsLoading = true;
getSocialActionByIssue(item.id).then(
(actions) =>
new Promise((resolve) => {
actions.results.forEach((action) => {
this.$store.commit("addActionInList", action);
}, this);
this.$store.commit("filterList", "actions");
this.$store.commit("filterList", "actions");
this.actionIsLoading = false;
this.actionAreLoaded = true;
resolve();
}),
);
}, this);
this.actionIsLoading = false;
this.actionAreLoaded = true;
resolve();
}),
);
}, this);
},
/* Reset socialActions List: flush list and restore selected actions
*/
resetActionsList() {
this.$store.commit("resetActionsList");
this.actionAreLoaded = false;
this.socialActionsSelected.forEach((item) => {
this.$store.commit("addActionInList", item);
}, this);
},
},
/* Reset socialActions List: flush list and restore selected actions
*/
resetActionsList() {
this.$store.commit("resetActionsList");
this.actionAreLoaded = false;
this.socialActionsSelected.forEach((item) => {
this.$store.commit("addActionInList", item);
}, this);
},
},
};
</script>
@@ -278,18 +284,18 @@ export default {
@import "ChillMainAssets/chill/scss/chill_variables";
span.multiselect__single {
display: none !important;
display: none !important;
}
#actionsList {
border-radius: 0.5rem;
padding: 1rem;
margin: 0.5rem;
background-color: whitesmoke;
border-radius: 0.5rem;
padding: 1rem;
margin: 0.5rem;
background-color: whitesmoke;
}
span.badge {
margin-bottom: 0.5rem;
@include badge_social($social-issue-color);
margin-bottom: 0.5rem;
@include badge_social($social-issue-color);
}
</style>

View File

@@ -44,21 +44,20 @@ span.badge {
@include badge_social($social-action-color);
font-size: 95%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;margin-bottom: 5px;
margin-right: 1em;
text-align: left;
line-height: 1.2em;
line-height: 1.2em;
&::before {
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
}
</style>

View File

@@ -42,21 +42,19 @@ span.badge {
@include badge_social($social-issue-color);
font-size: 95%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
margin-right: 1em;
text-align: left;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;margin-bottom: 5px;
margin-right: 1em;text-align: left;
&::before {
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
&::before {
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
}
</style>

View File

@@ -16,8 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
@@ -25,10 +24,9 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly ActivityRepository $repository,
Security $security,
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -39,7 +39,7 @@ final class Version20251118124241 extends AbstractMigration
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
SELECT id, user_id, true FROM activity WHERE user_id is not null
SELECT id, user_id, true FROM activity
ON CONFLICT DO NOTHING');
}

View File

@@ -21,10 +21,7 @@ use Doctrine\Persistence\ObjectManager;
class LoadAsideActivity extends Fixture implements DependentFixtureInterface
{
public function __construct(private readonly UserRepository $userRepository)
{
mt_srand(123456789);
}
public function __construct(private readonly UserRepository $userRepository) {}
public function getDependencies(): array
{
@@ -50,7 +47,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface
$this->getReference('aside_activity_category_0', AsideActivityCategory::class)
)
->setDate((new \DateTimeImmutable('today'))
->sub(new \DateInterval('P'.\mt_rand(1, 100).'D')));
->sub(new \DateInterval('P'.\random_int(1, 100).'D')));
$manager->persist($activity);
}

View File

@@ -56,7 +56,7 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
}
/** (non-PHPdoc).
* @see PrependExtensionInterface::prepend()
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prependRoutes(ContainerBuilder $container)
{

View File

@@ -72,20 +72,14 @@
{% macro table_results(actualCharges, actualResources, results) %}
{% set now = date() %}
{% set totalCharges = 0 %}
{% for c in actualCharges %}
{% if c.startDate <= now and (c.endDate is null or c.endDate >= now) %}
{% set totalCharges = totalCharges + c.amount %}
{% endif %}
{% set totalCharges = totalCharges + c.amount %}
{% endfor %}
{% set totalResources = 0 %}
{% for r in actualResources %}
{% if r.startDate <= now and (r.endDate is null or r.endDate >= now) %}
{% set totalResources = totalResources + r.amount %}
{% endif %}
{% set totalResources = totalResources + r.amount %}
{% endfor %}
{% set result = (totalResources - totalCharges) %}

View File

@@ -71,11 +71,4 @@ export function isEventInputCalendarRange(
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
}
export enum AnswerStatus {
ACCEPTED = "accepted",
DECLINED = "declined",
PENDING = "pending",
TENTATIVE = "tentative",
}
export {};

View File

@@ -1,148 +1,166 @@
<template>
<teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" />
<teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" />
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
: []
"
:removable-if-set="false"
:display-picked="false"
:suggested="this.suggestedUsers"
:label="'main_user'"
@add-new-entity="setMainUser"
/>
</div>
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
: []
"
:removable-if-set="false"
:display-picked="false"
:suggested="this.suggestedUsers"
:label="'main_user'"
@add-new-entity="setMainUser"
/>
</div>
</div>
</teleport>
</teleport>
<concerned-groups />
<concerned-groups />
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8">
{{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8">
{{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span
>
<span v-else>(Une plage de disponibilité sélectionnée)</span>
</div>
</div>
</teleport>
<location />
<teleport to="#fullCalendar">
<div class="calendar-actives">
<template v-for="u in getActiveUsers" :key="u.id">
<calendar-active
:user="u"
:invite="this.$store.getters.getInviteForUser(u)"
/>
</template>
</div>
<div
class="display-options row justify-content-between"
style="margin-top: 1rem"
>
<span v-else>(Une plage de disponibilité sélectionnée)</span>
</div>
</div>
</teleport>
<location />
<teleport to="#fullCalendar">
<div class="calendar-actives">
<template v-for="u in getActiveUsers" :key="u.id">
<calendar-active
:user="u"
:invite="this.$store.getters.getInviteForUser(u)"
/>
</template>
</div>
<div
class="display-options row justify-content-between"
style="margin-top: 1rem"
>
<div class="col-sm-9 col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select v-model="slotDuration" id="slotDuration" class="form-select">
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
<option value="00:45:00">45 minutes</option>
<option value="00:60:00">60 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
<option value="03:00:00">3h</option>
<option value="04:00:00">4h</option>
<option value="05:00:00">5h</option>
<option value="06:00:00">6h</option>
<option value="07:00:00">7h</option>
<option value="08:00:00">8h</option>
<option value="09:00:00">9h</option>
<option value="10:00:00">10h</option>
<option value="11:00:00">11h</option>
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
<option value="15:00:00">15h</option>
<option value="16:00:00">16h</option>
<option value="17:00:00">17h</option>
<option value="18:00:00">18h</option>
<option value="19:00:00">19h</option>
<option value="20:00:00">20h</option>
<option value="21:00:00">21h</option>
<option value="22:00:00">22h</option>
<option value="23:00:00">23h</option>
<option value="23:59:59">24h</option>
</select>
<div class="col-sm-9 col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
<option value="00:45:00">45 minutes</option>
<option value="00:60:00">60 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
<option value="03:00:00">3h</option>
<option value="04:00:00">4h</option>
<option value="05:00:00">5h</option>
<option value="06:00:00">6h</option>
<option value="07:00:00">7h</option>
<option value="08:00:00">8h</option>
<option value="09:00:00">9h</option>
<option value="10:00:00">10h</option>
<option value="11:00:00">11h</option>
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
<option value="15:00:00">15h</option>
<option value="16:00:00">16h</option>
<option value="17:00:00">17h</option>
<option value="18:00:00">18h</option>
<option value="19:00:00">19h</option>
<option value="20:00:00">20h</option>
<option value="21:00:00">21h</option>
<option value="22:00:00">22h</option>
<option value="23:00:00">23h</option>
<option value="23:59:59">24h</option>
</select>
</div>
</div>
<div class="col-sm-3 col-xs-12">
<div class="float-end">
<div class="form-check input-group">
<span class="input-group-text">
<input
id="showHideWE"
class="mt-0"
type="checkbox"
v-model="hideWeekends"
/>
</span>
<label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label
>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-3 col-xs-12">
<div class="float-end">
<div class="form-check input-group">
<span class="input-group-text">
<input
id="showHideWE"
class="mt-0"
type="checkbox"
v-model="hideWeekends"
/>
</span>
<label for="showHideWE" class="form-check-label input-group-text"
>Week-ends</label
>
</div>
</div>
</div>
</div>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template #eventContent="arg">
<span>
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }}
<small>{{ arg.event.extendedProps.userLabel }}</small></b
>
<b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
}}</b>
<b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b>
</span>
</template>
</FullCalendar>
</teleport>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template #eventContent="arg">
<span>
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }}
<small>{{
arg.event.extendedProps.userLabel
}}</small></b
>
<b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
}}</b>
<b v-else
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
</span>
</template>
</FullCalendar>
</teleport>
</template>
<script>
@@ -159,210 +177,219 @@ import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
import { mapGetters, mapState } from "vuex";
export default {
name: "App",
components: {
ConcernedGroups,
Location,
FullCalendar,
CalendarActive,
PickEntity,
},
data() {
return {
errorMsg: [],
showMyCalendar: false,
slotDuration: "00:05:00",
slotMinTime: "09:00:00",
slotMaxTime: "18:00:00",
hideWeekEnds: true,
previousUser: [],
};
},
computed: {
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
events() {
return this.$store.getters.getEventSources;
name: "App",
components: {
ConcernedGroups,
Location,
FullCalendar,
CalendarActive,
PickEntity,
},
calendarOptions() {
return {
locale: frLocale,
plugins: [
dayGridPlugin,
interactionPlugin,
timeGridPlugin,
dayGridPlugin,
listPlugin,
],
initialView: "timeGridWeek",
initialDate: this.$store.getters.getInitialDate,
eventSources: this.events,
selectable: true,
slotMinTime: this.slotMinTime,
slotMaxTime: this.slotMaxTime,
scrollTimeReset: false,
datesSet: this.onDatesSet,
select: this.onDateSelect,
eventChange: this.onEventChange,
eventClick: this.onEventClick,
selectMirror: true,
editable: true,
weekends: !this.hideWeekEnds,
headerToolbar: {
left: "prev,next today",
center: "title",
right: "timeGridWeek,timeGridDay,listWeek",
data() {
return {
errorMsg: [],
showMyCalendar: false,
slotDuration: "00:05:00",
slotMinTime: "09:00:00",
slotMaxTime: "18:00:00",
hideWeekEnds: true,
previousUser: [],
};
},
computed: {
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
events() {
return this.$store.getters.getEventSources;
},
views: {
timeGrid: {
slotEventOverlap: false,
slotDuration: this.slotDuration,
},
calendarOptions() {
return {
locale: frLocale,
plugins: [
dayGridPlugin,
interactionPlugin,
timeGridPlugin,
dayGridPlugin,
listPlugin,
],
initialView: "timeGridWeek",
initialDate: this.$store.getters.getInitialDate,
eventSources: this.events,
selectable: true,
slotMinTime: this.slotMinTime,
slotMaxTime: this.slotMaxTime,
scrollTimeReset: false,
datesSet: this.onDatesSet,
select: this.onDateSelect,
eventChange: this.onEventChange,
eventClick: this.onEventClick,
selectMirror: true,
editable: true,
weekends: !this.hideWeekEnds,
headerToolbar: {
left: "prev,next today",
center: "title",
right: "timeGridWeek,timeGridDay,listWeek",
},
views: {
timeGrid: {
slotEventOverlap: false,
slotDuration: this.slotDuration,
},
},
};
},
getActiveUsers() {
const users = [];
for (const id of this.$store.state.currentView.users.keys()) {
users.push(this.$store.getters.getUserDataById(id).user);
}
return users;
},
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u);
}
});
return suggested;
},
};
},
getActiveUsers() {
const users = [];
for (const id of this.$store.state.currentView.users.keys()) {
users.push(this.$store.getters.getUserDataById(id).user);
}
return users;
methods: {
setMainUser({ entity }) {
const user = entity;
console.log("setMainUser APP", entity);
if (
user.id !== this.$store.getters.getMainUser &&
(this.$store.state.activity.calendarRange !== null ||
this.$store.state.activity.startDate !== null ||
this.$store.state.activity.endDate !== null)
) {
if (
!window.confirm(
this.$t("change_main_user_will_reset_event_data"),
)
) {
return;
}
}
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(
this.$data.previousUser.map((u) => u.id),
);
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(
this.$store.getters.getMainUser,
);
}
}
this.$store.dispatch("setMainUser", user);
this.$store.commit("showUserOnCalendar", {
user,
ranges: true,
remotes: true,
});
},
removeMainUser(user) {
console.log("removeMainUser APP", user);
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log("onDatesSet", event);
this.$store.dispatch("setCurrentDatesView", {
start: event.start,
end: event.end,
});
},
onDateSelect(payload) {
console.log("onDateSelect", payload);
// show an alert if changing mainUser
if (
(this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !==
this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null
) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
return;
} else {
this.$store.commit("showUserOnCalendar", {
user: this.$store.state.me,
remotes: true,
ranges: true,
});
}
}
this.$store.dispatch("setEventTimes", {
start: payload.start,
end: payload.end,
});
},
onEventChange(payload) {
console.log("onEventChange", payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error(
"not allowed to edit a calendar associated with a calendar range",
);
}
this.$store.dispatch("setEventTimes", {
start: payload.event.start,
end: payload.event.end,
});
},
onEventClick(payload) {
if (payload.event.extendedProps.is !== "range") {
// do nothing when clicking on remote
return;
}
// show an alert if changing mainUser
if (
this.$store.getters.getMainUser !== null &&
payload.event.extendedProps.userId !==
this.$store.getters.getMainUser.id
) {
if (
!window.confirm(
this.$t("this_calendar_range_will_change_main_user"),
)
) {
return;
}
}
this.$store.dispatch("associateCalendarToRange", {
range: payload.event,
});
},
},
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u);
}
});
return suggested;
},
},
methods: {
setMainUser({ entity }) {
const user = entity;
console.log("setMainUser APP", entity);
if (
user.id !== this.$store.getters.getMainUser &&
(this.$store.state.activity.calendarRange !== null ||
this.$store.state.activity.startDate !== null ||
this.$store.state.activity.endDate !== null)
) {
if (
!window.confirm(this.$t("change_main_user_will_reset_event_data"))
) {
return;
}
}
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id));
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(this.$store.getters.getMainUser);
}
}
this.$store.dispatch("setMainUser", user);
this.$store.commit("showUserOnCalendar", {
user,
ranges: true,
remotes: true,
});
},
removeMainUser(user) {
console.log("removeMainUser APP", user);
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log("onDatesSet", event);
this.$store.dispatch("setCurrentDatesView", {
start: event.start,
end: event.end,
});
},
onDateSelect(payload) {
console.log("onDateSelect", payload);
// show an alert if changing mainUser
if (
(this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !== this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null
) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
return;
} else {
this.$store.commit("showUserOnCalendar", {
user: this.$store.state.me,
remotes: true,
ranges: true,
});
}
}
this.$store.dispatch("setEventTimes", {
start: payload.start,
end: payload.end,
});
},
onEventChange(payload) {
console.log("onEventChange", payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error(
"not allowed to edit a calendar associated with a calendar range",
);
}
this.$store.dispatch("setEventTimes", {
start: payload.event.start,
end: payload.event.end,
});
},
onEventClick(payload) {
if (payload.event.extendedProps.is !== "range") {
// do nothing when clicking on remote
return;
}
// show an alert if changing mainUser
if (
this.$store.getters.getMainUser !== null &&
payload.event.extendedProps.userId !==
this.$store.getters.getMainUser.id
) {
if (
!window.confirm(this.$t("this_calendar_range_will_change_main_user"))
) {
return;
}
}
this.$store.dispatch("associateCalendarToRange", {
range: payload.event,
});
},
},
};
</script>
<style>
.calendar-actives {
display: flex;
flex-direction: row;
flex-wrap: wrap;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.display-options {
margin-top: 1rem;
margin-top: 1rem;
}
/* for events which are range */
.fc-event.isrange {
border-width: 3px;
border-width: 3px;
}
</style>

View File

@@ -7,7 +7,7 @@
<i v-else-if="invite.status === 'declined'" class="fa fa-times" />
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" />
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" />
<span v-else>{{ invite.status }}</span>
<span v-else="">{{ invite.status }}</span>
</template>
</span>
<span class="form-check-inline form-switch">
@@ -42,6 +42,8 @@
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "CalendarActive",
props: {

View File

@@ -24,14 +24,6 @@ const appMessages = {
list_three_days: "Liste 3 jours",
current_selected: "Rendez-vous fixé",
main_user: "Utilisateur principal",
Give_an_answer: "Répondre",
Accepted: "Accepté",
Declined: "Refusé",
Tentative: "Accepté provisoirement",
Accept: "Accepter",
Decline: "Refuser",
Tentatively_accept: "Accepter provisoirement",
Set_pending: "Ne pas répondre",
},
};

View File

@@ -47,38 +47,77 @@
</div>
</template>
<script lang="ts" setup>
import { AnswerStatus } from "../../types";
<script lang="ts">
import { defineComponent, PropType } from "vue";
const props = defineProps<{
calendarId: number;
status: AnswerStatus;
}>();
const ACCEPTED = "accepted";
const DECLINED = "declined";
const PENDING = "pending";
const TENTATIVELY_ACCEPTED = "tentative";
const emit =
defineEmits<(e: "statusChanged", newStatus: AnswerStatus) => void>();
const Statuses = {
ACCEPTED: AnswerStatus.ACCEPTED,
DECLINED: AnswerStatus.DECLINED,
PENDING: AnswerStatus.PENDING,
TENTATIVELY_ACCEPTED: AnswerStatus.TENTATIVE,
const i18n = {
messages: {
fr: {
Give_an_answer: "Répondre",
Accepted: "Accepté",
Declined: "Refusé",
Tentative: "Accepté provisoirement",
Accept: "Accepter",
Decline: "Refuser",
Tentatively_accept: "Accepter provisoirement",
Set_pending: "Ne pas répondre",
},
},
};
function changeStatus(newStatus: AnswerStatus) {
const url = `/api/1.0/calendar/calendar/${props.calendarId}/answer/${newStatus}.json`;
window
.fetch(url, {
method: "POST",
})
.then((r: Response) => {
if (!r.ok) {
console.error("could not confirm answer", newStatus);
return;
}
emit("statusChanged", newStatus);
});
}
export default defineComponent({
name: "Answer",
i18n,
props: {
calendarId: { type: Number, required: true },
status: {
type: String as PropType<
"accepted" | "declined" | "pending" | "tentative"
>,
required: true,
},
},
emits: {
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
return true;
},
},
data() {
return {
Statuses: {
ACCEPTED,
DECLINED,
PENDING,
TENTATIVELY_ACCEPTED,
},
};
},
methods: {
changeStatus: function (
newStatus: "accepted" | "declined" | "pending" | "tentative",
) {
console.log("changeStatus", newStatus);
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
window
.fetch(url, {
method: "POST",
})
.then((r: Response) => {
if (!r.ok) {
console.error("could not confirm answer", newStatus);
return;
}
console.log("answer sent", newStatus);
this.$emit("statusChanged", newStatus);
});
},
},
});
</script>
<style scoped></style>

View File

@@ -346,7 +346,6 @@ const baseOptions = ref<CalendarOptions>({
center: "title",
right: "timeGridWeek,timeGridDay",
},
allDaySlot: false,
});
const ranges = computed<EventInput[]>(() => {

View File

@@ -74,12 +74,12 @@ const saveAndClose = function (e: Event): void {
location: location.value,
calendarRangeId: calendarRangeId.value,
})
.then(() => {
.then((_) => {
showModal.value = false;
});
};
const closeModal = function (): void {
const closeModal = function (_: any): void {
showModal.value = false;
};

View File

@@ -78,7 +78,7 @@
</div>
{% if calendar.comment.comment is not empty
or calendar.persons|length > 0
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0 %}
<div class="item-row details separator">

View File

@@ -41,7 +41,6 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
public function __construct()
{
mt_srand(123456789);
$this->fakerFr = \Faker\Factory::create('fr_FR');
$this->fakerEn = \Faker\Factory::create('en_EN');
$this->fakerNl = \Faker\Factory::create('nl_NL');
@@ -105,7 +104,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($parent);
// Load children
$expected_nb_children = mt_rand(10, 50);
$expected_nb_children = random_int(10, 50);
for ($i = 0; $i < $expected_nb_children; ++$i) {
$companyName = $this->fakerFr->company;
@@ -145,7 +144,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($parent);
// Load children
$expected_nb_children = mt_rand(10, 50);
$expected_nb_children = random_int(10, 50);
for ($i = 0; $i < $expected_nb_children; ++$i) {
$manager->persist($this->createChildOption($parent, [

View File

@@ -52,7 +52,7 @@ class ChillCustomFieldsExtension extends Extension implements PrependExtensionIn
}
/** (non-PHPdoc).
* @see PrependExtensionInterface::prepend()
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prepend(ContainerBuilder $container)
{

View File

@@ -25,7 +25,7 @@ class ChoiceWithOtherType extends AbstractType
private string $otherValueLabel = 'Other value';
/** (non-PHPdoc).
* @see AbstractType::buildForm()
* @see \Symfony\Component\Form\AbstractType::buildForm()
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -42,7 +42,7 @@ class ChoiceWithOtherType extends AbstractType
}
/** (non-PHPdoc).
* @see AbstractType::configureOptions()
* @see \Symfony\Component\Form\AbstractType::configureOptions()
*/
public function configureOptions(OptionsResolver $resolver)
{

View File

@@ -22,7 +22,7 @@ use Symfony\Component\Form\FormEvents;
class ChoicesListType extends AbstractType
{
/** (non-PHPdoc).
* @see AbstractType::buildForm()
* @see \Symfony\Component\Form\AbstractType::buildForm()
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{

View File

@@ -82,7 +82,7 @@ class CustomFieldProvider implements ContainerAwareInterface
/**
* (non-PHPdoc).
*
* @see ContainerAwareInterface::setContainer()
* @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer()
*/
public function setContainer(?ContainerInterface $container = null)
{

View File

@@ -2,11 +2,12 @@ import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
import { createApp } from "vue";
import { StoredObject, StoredObjectStatusChange } from "../../types";
import { is_object_ready } from "../../vuejs/StoredObjectButton/helpers";
import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({});
window.addEventListener("DOMContentLoaded", function () {
window.addEventListener("DOMContentLoaded", function (e) {
document
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
.forEach((el) => {

View File

@@ -2,7 +2,7 @@ import { DateTime } from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types/index";
export interface GenericDocMetadata {
isPresent: boolean;
isPresent: boolean;
}
/**
@@ -15,69 +15,69 @@ export interface EmptyMetadata extends GenericDocMetadata {}
* Minimal Metadata for a GenericDoc with a normalizer
*/
export interface BaseMetadata extends GenericDocMetadata {
title: string;
title: string;
}
/**
* A generic doc is a document attached to a Person or an AccompanyingPeriod.
*/
export interface GenericDoc {
type: "doc_store_generic_doc";
uniqueKey: string;
key: string;
identifiers: { id: number };
context: "person" | "accompanying-period";
doc_date: DateTime;
metadata: GenericDocMetadata;
storedObject: StoredObject | null;
type: "doc_store_generic_doc";
uniqueKey: string;
key: string;
identifiers: { id: number };
context: "person" | "accompanying-period";
doc_date: DateTime;
metadata: GenericDocMetadata;
storedObject: StoredObject | null;
}
export interface GenericDocForAccompanyingPeriod extends GenericDoc {
context: "accompanying-period";
context: "accompanying-period";
}
export function isGenericDocForAccompanyingPeriod(
doc: GenericDoc,
doc: GenericDoc,
): doc is GenericDocForAccompanyingPeriod {
return doc.context === "accompanying-period";
return doc.context === "accompanying-period";
}
export function isGenericDocWithStoredObject(
doc: GenericDoc,
doc: GenericDoc,
): doc is GenericDoc & { storedObject: StoredObject } {
return doc.storedObject !== null;
return doc.storedObject !== null;
}
interface BaseMetadataWithHtml extends BaseMetadata {
html: string;
html: string;
}
export interface GenericDocForAccompanyingCourseDocument extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
key: "accompanying_course_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseActivityDocument extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_activity_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
key: "accompanying_course_activity_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseCalendarDocument extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_calendar_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
key: "accompanying_course_calendar_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCoursePersonDocument extends GenericDocForAccompanyingPeriod {
key: "person_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
key: "person_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument extends GenericDocForAccompanyingPeriod {
key: "accompanying_period_work_evaluation_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
key: "accompanying_period_work_evaluation_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}

View File

@@ -9,7 +9,6 @@ export interface StoredObject {
uuid: string;
prefix: string;
status: StoredObjectStatus;
type: string;
currentVersion:
| null
| StoredObjectVersionCreated
@@ -47,7 +46,8 @@ export interface StoredObjectVersionCreated extends StoredObjectVersion {
persisted: false;
}
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
export interface StoredObjectVersionPersisted
extends StoredObjectVersionCreated {
version: number;
id: number;
createdAt: DateTime | null;
@@ -61,7 +61,8 @@ export interface StoredObjectStatusChange {
type: string;
}
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
export interface StoredObjectVersionWithPointInTime
extends StoredObjectVersionPersisted {
"point-in-times": StoredObjectPointInTime[];
"from-restored": StoredObjectVersionPersisted | null;
}

View File

@@ -26,8 +26,8 @@
<li v-if="isEditableOnDesktop">
<desktop-edit-button
:classes="{ 'dropdown-item': true }"
:edit-link="props.davLink ?? ''"
:expiration-link="props.davLinkExpiration ?? 0"
:edit-link="props.davLink"
:expiration-link="props.davLinkExpiration"
></desktop-edit-button>
</li>
<li v-if="isConvertibleToPdf">
@@ -75,6 +75,7 @@ import {
import {
StoredObject,
StoredObjectStatusChange,
StoredObjectVersion,
WopiEditButtonExecutableBeforeLeaveFunction,
} from "../types";
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
@@ -205,6 +206,10 @@ const checkForReady = function (): void {
};
const onObjectNewStatusCallback = async function (): Promise<void> {
if (props.storedObject.status === "stored_object_created") {
return Promise.resolve();
}
const new_status = await is_object_ready(props.storedObject);
if (props.storedObject.status !== new_status.status) {
emit("onStoredObjectStatusChange", new_status);

View File

@@ -1,35 +1,3 @@
<template>
<div class="drop-file">
<div
v-if="!uploading"
:class="{ area: true, dragging: is_dragging }"
@click="onZoneClick"
@dragover="onDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<p v-if="has_existing_doc" class="file-icon">
<file-icon :type="props.existingDoc?.type ?? ''"></file-icon>
</p>
<p v-if="display_filename !== null" class="display-filename">
{{ display_filename }}
</p>
<!-- todo i18n -->
<p v-if="has_existing_doc">
Déposez un document ou cliquez ici pour remplacer le document existant
</p>
<p v-else>
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier
</p>
</div>
<div v-else class="waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { StoredObject, StoredObjectVersionCreated } from "../../types";
import {
@@ -41,23 +9,24 @@ import { computed, ref, Ref } from "vue";
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
interface DropFileConfig {
existingDoc: StoredObject | null;
existingDoc?: StoredObject;
}
const props = withDefaults(defineProps<DropFileConfig>(), {
existingDoc: null,
});
const emit = defineEmits<
(
e: "addDocument",
payload: {
stored_object_version: StoredObjectVersionCreated;
stored_object: StoredObject;
file_name: string;
},
) => void
>();
const emit =
defineEmits<
(
e: "addDocument",
{
stored_object_version: StoredObjectVersionCreated,
stored_object: StoredObject,
file_name: string,
},
) => void
>();
const is_dragging: Ref<boolean> = ref(false);
const uploading: Ref<boolean> = ref(false);
@@ -165,6 +134,38 @@ const handleFile = async (file: File): Promise<void> => {
};
</script>
<template>
<div class="drop-file">
<div
v-if="!uploading"
:class="{ area: true, dragging: is_dragging }"
@click="onZoneClick"
@dragover="onDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<p v-if="has_existing_doc" class="file-icon">
<file-icon :type="props.existingDoc?.type"></file-icon>
</p>
<p v-if="display_filename !== null" class="display-filename">
{{ display_filename }}
</p>
<!-- todo i18n -->
<p v-if="has_existing_doc">
Déposez un document ou cliquez ici pour remplacer le document existant
</p>
<p v-else>
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier
</p>
</div>
<div v-else class="waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
</div>
</template>
<style scoped lang="scss">
.drop-file {
width: 100%;

View File

@@ -7,24 +7,24 @@ import { useToast } from "vue-toast-notification";
import { DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig {
allowRemove: boolean;
existingDoc?: StoredObject;
allowRemove: boolean;
existingDoc?: StoredObject;
}
const props = withDefaults(defineProps<DropFileConfig>(), {
allowRemove: false,
allowRemove: false,
});
const emit = defineEmits<{
(
e: "addDocument",
payload: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
},
): void;
(e: "removeDocument"): void;
(
e: "addDocument",
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
file_name: string,
},
): void;
(e: "removeDocument"): void;
}>();
const $toast = useToast();
@@ -34,65 +34,65 @@ const state = reactive({ showModal: false });
const modalClasses = { "modal-dialog-centered": true, "modal-md": true };
const buttonState = computed<"add" | "replace">(() => {
if (props.existingDoc === undefined || props.existingDoc === null) {
return "add";
}
if (props.existingDoc === undefined || props.existingDoc === null) {
return "add";
}
return "replace";
return "replace";
});
function onAddDocument({
stored_object,
stored_object_version,
file_name,
stored_object,
stored_object_version,
file_name,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
}): void {
const message =
buttonState.value === "add" ? "Document ajouté" : "Document remplacé";
$toast.success(message);
emit("addDocument", { stored_object_version, stored_object, file_name });
state.showModal = false;
const message =
buttonState.value === "add" ? "Document ajouté" : "Document remplacé";
$toast.success(message);
emit("addDocument", { stored_object_version, stored_object, file_name });
state.showModal = false;
}
function onRemoveDocument(): void {
emit("removeDocument");
emit("removeDocument");
}
function openModal(): void {
state.showModal = true;
state.showModal = true;
}
function closeModal(): void {
state.showModal = false;
state.showModal = false;
}
</script>
<template>
<button
v-if="buttonState === 'add'"
@click="openModal"
class="btn btn-create"
>
{{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit"></button>
<modal
v-if="state.showModal"
:modal-dialog-class="modalClasses"
@close="closeModal"
>
<template v-slot:body>
<drop-file-widget
:existing-doc="existingDoc"
:allow-remove="allowRemove"
@add-document="onAddDocument"
@remove-document="onRemoveDocument"
></drop-file-widget>
</template>
</modal>
<button
v-if="buttonState === 'add'"
@click="openModal"
class="btn btn-create"
>
{{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit"></button>
<modal
v-if="state.showModal"
:modal-dialog-class="modalClasses"
@close="closeModal"
>
<template v-slot:body>
<drop-file-widget
:existing-doc="existingDoc"
:allow-remove="allowRemove"
@add-document="onAddDocument"
@remove-document="onRemoveDocument"
></drop-file-widget>
</template>
</modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,34 +1,6 @@
<template>
<div>
<drop-file
:existingDoc="props.existingDoc ?? null"
@addDocument="onAddDocument"
></drop-file>
<ul class="record_actions">
<li v-if="props?.existingDoc">
<document-action-buttons-group
:stored-object="props.existingDoc"
:can-edit="props.existingDoc?.status === 'ready'"
:can-download="true"
:dav-link="dav_link_href ?? ''"
:dav-link-expiration="dav_link_expiration ?? 0"
/>
</li>
<li>
<button
v-if="allowRemove"
class="btn btn-delete"
@click="onRemoveDocument($event)"
></button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { StoredObject, StoredObjectVersion } from "../../types";
import { computed } from "vue";
import { computed, ref, Ref } from "vue";
import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
@@ -44,15 +16,19 @@ const props = withDefaults(defineProps<DropFileConfig>(), {
const emit = defineEmits<{
(
e: "addDocument",
payload: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
file_name: string,
},
): void;
(e: "removeDocument"): void;
}>();
const has_existing_doc = computed<boolean>(() => {
return props.existingDoc !== undefined && props.existingDoc !== null;
});
const dav_link_expiration = computed<number | undefined>(() => {
if (props.existingDoc === undefined || props.existingDoc === null) {
return undefined;
@@ -93,4 +69,33 @@ const onRemoveDocument = (e: Event): void => {
emit("removeDocument");
};
</script>
<template>
<div>
<drop-file
:existingDoc="props.existingDoc"
@addDocument="onAddDocument"
></drop-file>
<ul class="record_actions">
<li v-if="has_existing_doc">
<document-action-buttons-group
:stored-object="props.existingDoc"
:can-edit="props.existingDoc?.status === 'ready'"
:can-download="true"
:dav-link="dav_link_href"
:dav-link-expiration="dav_link_expiration"
/>
</li>
<li>
<button
v-if="allowRemove"
class="btn btn-delete"
@click="onRemoveDocument($event)"
></button>
</li>
</ul>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,3 +1,11 @@
<script setup lang="ts">
interface FileIconConfig {
type: string;
}
const props = defineProps<FileIconConfig>();
</script>
<template>
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
<i
@@ -35,12 +43,4 @@
<i class="fa fa-file-code-o" v-else></i>
</template>
<script setup lang="ts">
interface FileIconConfig {
type: string;
}
const props = defineProps<FileIconConfig>();
</script>
<style scoped lang="scss"></style>

View File

@@ -1,3 +1,42 @@
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { computed, reactive } from "vue";
export interface DesktopEditButtonConfig {
editLink: null;
classes: Record<string, boolean>;
expirationLink: number | Date;
}
interface DesktopEditButtonState {
modalOpened: boolean;
}
const state: DesktopEditButtonState = reactive({ modalOpened: false });
const props = defineProps<DesktopEditButtonConfig>();
const buildCommand = computed<string>(
() => "vnd.libreoffice.command:ofe|u|" + props.editLink,
);
const editionUntilFormatted = computed<string>(() => {
let d;
if (props.expirationLink instanceof Date) {
d = props.expirationLink;
} else {
d = new Date(props.expirationLink * 1000);
}
console.log(props.expirationLink);
return new Intl.DateTimeFormat(undefined, {
dateStyle: "long",
timeStyle: "medium",
}).format(d);
});
</script>
<template>
<teleport to="body">
<modal v-if="state.modalOpened" @close="state.modalOpened = false">
@@ -51,41 +90,3 @@ i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { computed, reactive } from "vue";
export interface DesktopEditButtonConfig {
editLink: string;
classes: Record<string, boolean>;
expirationLink: number | Date;
}
interface DesktopEditButtonState {
modalOpened: boolean;
}
const state: DesktopEditButtonState = reactive({ modalOpened: false });
const props = defineProps<DesktopEditButtonConfig>();
const buildCommand = computed<string>(
() => "vnd.libreoffice.command:ofe|u|" + props.editLink,
);
const editionUntilFormatted = computed<string>(() => {
let d;
if (props.expirationLink instanceof Date) {
d = props.expirationLink;
} else {
d = new Date(props.expirationLink * 1000);
}
return new Intl.DateTimeFormat(undefined, {
dateStyle: "long",
timeStyle: "medium",
}).format(d);
});
</script>

View File

@@ -12,7 +12,7 @@
v-else
:class="props.classes"
target="_blank"
:type="props.atVersion?.type"
:type="props.atVersion.type"
:download="buildDocumentName()"
:href="state.href_url"
ref="open_button"
@@ -27,15 +27,11 @@
import { reactive, ref, nextTick, onMounted } from "vue";
import { download_and_decrypt_doc } from "./helpers";
import mime from "mime";
import {
StoredObject,
StoredObjectVersionCreated,
StoredObjectVersionPersisted,
} from "../../types";
import { StoredObject, StoredObjectVersion } from "../../types";
interface DownloadButtonConfig {
storedObject: StoredObject;
atVersion: null | StoredObjectVersionCreated | StoredObjectVersionPersisted;
atVersion: StoredObjectVersion;
classes: Record<string, boolean>;
filename?: string;
/**
@@ -74,7 +70,7 @@ function buildDocumentName(): string {
document_name = "document";
}
const ext = mime.getExtension(props.atVersion?.type ?? "");
const ext = mime.getExtension(props.atVersion.type);
if (null !== ext) {
return document_name + "." + ext;

View File

@@ -1,24 +1,10 @@
<template>
<a @click="download_version_and_open_modal" class="dropdown-item">
<history-button-modal
ref="modal"
:versions="state.versions"
:stored-object="storedObject"
:can-edit="canEdit"
@restore-version="onRestoreVersion"
></history-button-modal>
<i class="fa fa-history"></i>
Historique
</a>
</template>
<script setup lang="ts">
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
import {
StoredObject,
StoredObjectVersionWithPointInTime,
} from "./../../types";
import { reactive, useTemplateRef } from "vue";
import { computed, reactive, ref, useTemplateRef } from "vue";
import { get_versions } from "./HistoryButton/api";
interface HistoryButtonConfig {
@@ -52,11 +38,29 @@ const download_version_and_open_modal = async function (): Promise<void> {
}
};
const onRestoreVersion = (newVersion: StoredObjectVersionWithPointInTime) => {
const onRestoreVersion = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
state.versions.unshift(newVersion);
};
</script>
<template>
<a @click="download_version_and_open_modal" class="dropdown-item">
<history-button-modal
ref="modal"
:versions="state.versions"
:stored-object="storedObject"
:can-edit="canEdit"
@restore-version="onRestoreVersion"
></history-button-modal>
<i class="fa fa-history"></i>
Historique
</a>
</template>
<style scoped lang="scss">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);

View File

@@ -1,22 +1,3 @@
<template>
<template v-if="props.versions.length > 0">
<div class="container">
<template v-for="v in props.versions" :key="v.id">
<history-button-list-item
:version="v"
:can-edit="canEdit"
:is-current="higher_version === v.version"
:stored-object="storedObject"
@restore-version="onRestored"
></history-button-list-item>
</template>
</div>
</template>
<template v-else>
<p>Chargement des versions</p>
</template>
</template>
<script setup lang="ts">
import {
StoredObject,
@@ -59,10 +40,33 @@ const higher_version = computed<number>(() =>
*
* internally, keep track of the newly restored version
*/
const onRestored = (newVersion: StoredObjectVersionWithPointInTime) => {
const onRestored = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
state.restored = newVersion.version;
emit("restoreVersion", newVersion);
emit("restoreVersion", { newVersion });
};
</script>
<template>
<template v-if="props.versions.length > 0">
<div class="container">
<template v-for="v in props.versions" :key="v.id">
<history-button-list-item
:version="v"
:can-edit="canEdit"
:is-current="higher_version === v.version"
:stored-object="storedObject"
@restore-version="onRestored"
></history-button-list-item>
</template>
</div>
</template>
<template v-else>
<p>Chargement des versions</p>
</template>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,75 +1,8 @@
<template>
<div :class="classes">
<div
class="col-12 tags"
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated"
>
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
<span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span
>
<span class="badge bg-info" v-if="isRestored"
>Restaurée depuis la version
{{
version["from-restored"]?.version
? version["from-restored"]?.version + 1
: ""
}}</span
>
<span class="badge bg-info" v-if="isDuplicated"
>Dupliqué depuis un autre document</span
>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
>
<template v-if="version.createdBy !== null && version.createdAt !== null">
<strong v-if="version.version == 0">créé par</strong>
<strong v-else>modifié par</strong>
<span class="badge-user">
<UserRenderBoxBadge :user="version.createdBy" />
</span>
<strong>à</strong>
{{ $d(ISOToDatetime(version.createdAt.datetime8601) ?? 0, "long") }}
</template>
<template v-if="version.createdBy === null && version.createdAt !== null">
<strong v-if="version.version == 0">Créé le</strong>
<strong v-else>modifié le</strong>
{{ $d(ISOToDatetime(version.createdAt.datetime8601) ?? 0, "long") }}
</template>
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
<li v-if="canEdit && !isCurrent">
<restore-version-button
:stored-object-version="props.version"
@restore-version="onRestore"
></restore-version-button>
</li>
<li>
<download-button
:stored-object="storedObject"
:at-version="version"
:classes="{
btn: true,
'btn-outline-primary': true,
'btn-sm': true,
}"
:display-action-string-in-button="false"
></download-button>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import {
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
} from "ChillDocStoreAssets/types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
@@ -79,103 +12,185 @@ import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/Downloa
import { computed } from "vue";
interface HistoryButtonListItemConfig {
version: StoredObjectVersionWithPointInTime;
storedObject: StoredObject;
canEdit: boolean;
isCurrent: boolean;
version: StoredObjectVersionWithPointInTime;
storedObject: StoredObject;
canEdit: boolean;
isCurrent: boolean;
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
const props = defineProps<HistoryButtonListItemConfig>();
const onRestore = (newVersion: StoredObjectVersionWithPointInTime) => {
emit("restoreVersion", newVersion);
const onRestore = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
emit("restoreVersion", { newVersion });
};
const isKeptBeforeConversion = computed<boolean>(() => {
if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
);
} else {
return false;
}
if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
);
} else {
return false;
}
});
const isRestored = computed<boolean>(
() => props.version.version > 0 && null !== props.version["from-restored"],
() => props.version.version > 0 && null !== props.version["from-restored"],
);
const isDuplicated = computed<boolean>(
() => props.version.version === 0 && null !== props.version["from-restored"],
() =>
props.version.version === 0 && null !== props.version["from-restored"],
);
const classes = computed<{
row: true;
"row-hover": true;
"blinking-1": boolean;
"blinking-2": boolean;
row: true;
"row-hover": true;
"blinking-1": boolean;
"blinking-2": boolean;
}>(() => ({
row: true,
"row-hover": true,
"blinking-1": isRestored.value && 0 === props.version.version % 2,
"blinking-2": isRestored.value && 1 === props.version.version % 2,
row: true,
"row-hover": true,
"blinking-1": props.isRestored && 0 === props.version.version % 2,
"blinking-2": props.isRestored && 1 === props.version.version % 2,
}));
</script>
<template>
<div :class="classes">
<div
class="col-12 tags"
v-if="
isCurrent ||
isKeptBeforeConversion ||
isRestored ||
isDuplicated
"
>
<span class="badge bg-success" v-if="isCurrent"
>Version actuelle</span
>
<span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span
>
<span class="badge bg-info" v-if="isRestored"
>Restaurée depuis la version
{{ version["from-restored"]?.version + 1 }}</span
>
<span class="badge bg-info" v-if="isDuplicated"
>Dupliqué depuis un autre document</span
>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
>
<template
v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">créé par</strong
><strong v-else>modifié par</strong>
<span class="badge-user"
><UserRenderBoxBadge
:user="version.createdBy"
></UserRenderBoxBadge
></span>
<strong>à</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
><template
v-if="version.createdBy === null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé le</strong
><strong v-else>modifié le</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
>
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
<li v-if="canEdit && !isCurrent">
<restore-version-button
:stored-object-version="props.version"
@restore-version="onRestore"
></restore-version-button>
</li>
<li>
<download-button
:stored-object="storedObject"
:at-version="version"
:classes="{
btn: true,
'btn-outline-primary': true,
'btn-sm': true,
}"
:display-action-string-in-button="false"
></download-button>
</li>
</ul>
</div>
</div>
</template>
<style scoped lang="scss">
div.tags {
span.badge:not(:last-child) {
margin-right: 0.5rem;
}
span.badge:not(:last-child) {
margin-right: 0.5rem;
}
}
// to make the animation restart, we have the same animation twice,
// and alternate between both
.blinking-1 {
animation-name: backgroundColorPalette-1;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
animation-name: backgroundColorPalette-1;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
}
@keyframes backgroundColorPalette-1 {
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
}
.blinking-2 {
animation-name: backgroundColorPalette-2;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
animation-name: backgroundColorPalette-2;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
}
@keyframes backgroundColorPalette-2 {
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
}
</style>

View File

@@ -1,22 +1,3 @@
<template>
<Teleport to="body">
<modal v-if="state.opened" @close="state.opened = false">
<template v-slot:header>
<h3>Historique des versions du document</h3>
</template>
<template v-slot:body>
<p>Les versions sont conservées pendant 90 jours.</p>
<history-button-list
:versions="props.versions"
:can-edit="canEdit"
:stored-object="storedObject"
@restore-version="onRestoreVersion"
></history-button-list>
</template>
</modal>
</Teleport>
</template>
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { reactive } from "vue";
@@ -47,10 +28,29 @@ const open = () => {
state.opened = true;
};
const onRestoreVersion = (newVersion: StoredObjectVersionWithPointInTime) =>
emit("restoreVersion", newVersion);
const onRestoreVersion = (payload: {
newVersion: StoredObjectVersionWithPointInTime;
}) => emit("restoreVersion", payload);
defineExpose({ open });
</script>
<template>
<Teleport to="body">
<modal v-if="state.opened" @close="state.opened = false">
<template v-slot:header>
<h3>Historique des versions du document</h3>
</template>
<template v-slot:body>
<p>Les versions sont conservées pendant 90 jours.</p>
<history-button-list
:versions="props.versions"
:can-edit="canEdit"
:stored-object="storedObject"
@restore-version="onRestoreVersion"
></history-button-list>
</template>
</modal>
</Teleport>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,13 +1,3 @@
<template>
<button
class="btn btn-outline-action"
@click="restore_version_fn"
title="Restaurer"
>
<i class="fa fa-rotate-left"></i> Restaurer
</button>
</template>
<script setup lang="ts">
import {
StoredObjectVersionPersisted,
@@ -32,8 +22,18 @@ const restore_version_fn = async () => {
const newVersion = await restore_version(props.storedObjectVersion);
$toast.success("Version restaurée");
emit("restoreVersion", newVersion);
emit("restoreVersion", { newVersion });
};
</script>
<template>
<button
class="btn btn-outline-action"
@click="restore_version_fn"
title="Restaurer"
>
<i class="fa fa-rotate-left"></i> Restaurer
</button>
</template>
<style scoped lang="scss"></style>

View File

@@ -15,10 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@@ -37,8 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function __construct(
private readonly Security $security,
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {}
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
@@ -50,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
{
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
$workflowPermissionAsAttachment = match ($attribute) {
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
};
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
return false;
}
// Retrieve the related entity
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
@@ -59,7 +65,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
if (!$this->canBeAssociatedWithWorkflow()) {
return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject);
return $regularPermission;
}
$workflowPermission = match ($attribute) {
@@ -68,41 +74,9 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
};
return match ($workflowPermission) {
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false,
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject),
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
};
}
private function voteOnStoredObjectAsAttachementOfAWorkflow(StoredObjectRoleEnum $attribute, bool $regularPermission, StoredObject $storedObject): bool
{
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($storedObject);
// we get all the entity workflows where the stored object is attached
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
// we compute all the permission for each entity workflow
$permissions = array_map(fn (EntityWorkflow $entityWorkflow): string => match ($attribute) {
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entityWorkflow),
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entityWorkflow),
}, $entityWorkflows);
// now, we reduce the permissions: abstain are ignored. Between DENIED and and GRANT, DENIED takes precedence
$computedPermission = WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN;
foreach ($permissions as $permission) {
if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED === $permission) {
return false;
}
if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT === $permission) {
$computedPermission = WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT;
}
}
if (WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN === $computedPermission) {
return $regularPermission;
}
// this is the case where WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT is returned
return true;
}
}

View File

@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
@@ -26,9 +25,8 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
private readonly AccompanyingCourseDocumentRepository $repository,
Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) {
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
@@ -26,9 +25,8 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
private readonly PersonDocumentRepository $repository,
Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) {
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -16,11 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security;
@@ -34,31 +31,21 @@ class AbstractStoredObjectVoterTest extends TestCase
{
use ProphecyTrait;
/**
* @param array<int, EntityWorkflowAttachment> $attachments
*
* @return void
*/
private function buildStoredObjectVoter(
bool $canBeAssociatedWithWorkflow,
AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null,
array $attachments = [],
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
): AbstractStoredObjectVoter {
$attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
$attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
// Anonymous class extending the abstract class
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter {
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
public function __construct(
private readonly bool $canBeAssociatedWithWorkflow,
private readonly AssociatedEntityToStoredObjectInterface $repository,
Security $security,
EntityWorkflowAttachmentRepository $attachmentRepository,
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
parent::__construct($security, $workflowDocumentService);
}
protected function attributeToRole($attribute): string
@@ -85,29 +72,28 @@ class AbstractStoredObjectVoterTest extends TestCase
public function testSupportsOnAttribute(): void
{
$entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
}
/**
* @dataProvider dataProviderVoteOnAttributeWithWorkflow
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
*/
public function testVoteOnAttributeWithWorkflow(
public function testVoteOnAttributeWithStoredObjectPermission(
StoredObjectRoleEnum $attribute,
bool $expected,
bool $isGrantedRegularPermission,
string $isGrantedWorkflowPermission,
string $isGrantedStoredObjectAttachment,
): void {
$storedObject = new StoredObject();
$repository = new DummyRepository($related = new \stdClass());
@@ -116,28 +102,31 @@ class AbstractStoredObjectVoterTest extends TestCase
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
$attachementRepository->findByStoredObject($storedObject)->willReturn([]);
if (StoredObjectRoleEnum::SEE === $attribute) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
->shouldBeCalled()
->willReturn($isGrantedStoredObjectAttachment);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
->willReturn($isGrantedWorkflowPermission);
} elseif (StoredObjectRoleEnum::EDIT === $attribute) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
->shouldBeCalled()
->willReturn($isGrantedStoredObjectAttachment);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
->willReturn($isGrantedWorkflowPermission);
} else {
throw new \LogicException('Invalid attribute for StoredObjectVoter');
}
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter {
public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository)
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
public function __construct(private $repository, $helper, $security)
{
parent::__construct($security, $attachmentRepository, $helper);
parent::__construct($security, $helper);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
@@ -166,64 +155,96 @@ class AbstractStoredObjectVoterTest extends TestCase
self::assertEquals($expected, $actual);
}
public static function dataProviderVoteOnAttributeWithWorkflow(): iterable
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
{
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
yield 'Not related to any workflow nor attachment ('.$action.')' => [
$attribute,
true,
true,
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
];
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
$attribute,
false,
false,
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
];
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
$attribute,
false,
true,
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
];
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
$attribute,
false,
true,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
];
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
$attribute,
false,
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
];
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
$attribute,
false,
true,
true,
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
];
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
$attribute,
false,
false,
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
];
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
$attribute,
false,
false,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
];
yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
$attribute,
true,
false,
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
];
yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
$attribute,
true,
false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
];
}
}
/**
* @dataProvider dataProviderVoteOnAttribute
* @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
*/
public function testVoteOnAttribute(
public function testVoteOnAttributeWithoutStoredObjectPermission(
StoredObjectRoleEnum $attribute,
bool $expected,
bool $canBeAssociatedWithWorkflow,
@@ -239,7 +260,10 @@ class AbstractStoredObjectVoterTest extends TestCase
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
if (null !== $isGrantedWorkflowPermissionRead) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
@@ -259,155 +283,27 @@ class AbstractStoredObjectVoterTest extends TestCase
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
}
public static function dataProviderVoteOnAttribute(): iterable
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
{
// not associated on a workflow
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
// associated on a workflow, read operation
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
// association on a workflow, write operation
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
}
/**
* @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments
*/
public function testPrecedenceOfDirectAssociationOverWorkflowAttachments(
StoredObjectRoleEnum $attribute,
bool $expected,
bool $regularPermission,
string $directWorkflowPermission,
string $attachmentWorkflowPermission,
string $message,
): void {
$storedObject = new StoredObject();
$repository = new DummyRepository($related = new \stdClass());
$token = new UsernamePasswordToken(new User(), 'dummy');
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($regularPermission);
$workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
// Direct association permission
if (StoredObjectRoleEnum::SEE === $attribute) {
$workflowHelper->isAllowedByWorkflowForReadOperation($related)
->willReturn($directWorkflowPermission);
} else {
$workflowHelper->isAllowedByWorkflowForWriteOperation($related)
->willReturn($directWorkflowPermission);
}
// Attachment permission
$entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal();
$attachment = $this->prophesize(EntityWorkflowAttachment::class);
$attachment->getEntityWorkflow()->willReturn($entityWorkflow);
if (StoredObjectRoleEnum::SEE === $attribute) {
$workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow)
->willReturn($attachmentWorkflowPermission);
} else {
$workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow)
->willReturn($attachmentWorkflowPermission);
}
$voter = $this->buildStoredObjectVoter(
true,
$repository,
$security->reveal(),
$workflowHelper->reveal(),
[$attachment->reveal()]
);
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
}
public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable
{
$cases = [
[
'expected' => true,
'regular' => false,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED',
],
[
'expected' => false,
'regular' => true,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT',
],
[
'expected' => true,
'regular' => false,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN',
],
[
'expected' => false,
'regular' => true,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN',
],
[
'expected' => true,
'regular' => false,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win',
],
[
'expected' => false,
'regular' => true,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win',
],
[
'expected' => true,
'regular' => true,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'message' => 'Both ABSTAIN should let regular permission (true) win',
],
[
'expected' => false,
'regular' => false,
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
'message' => 'Both ABSTAIN should let regular permission (false) win',
],
];
foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) {
foreach ($cases as $case) {
yield sprintf('%s - %s', $attribute->name, $case['message']) => [
$attribute,
$case['expected'],
$case['regular'],
$case['direct'],
$case['attachment'],
$case['message'],
];
}
}
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
}
}

View File

@@ -34,7 +34,6 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
public function __construct()
{
mt_srand(123456789);
$this->faker = \Faker\Factory::create('fr_FR');
}
@@ -46,7 +45,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
for ($i = 0; $i < $expectedNumber; ++$i) {
$event = (new Event())
->setDate($this->faker->dateTimeBetween('-2 years', '+6 months'))
->setName($this->faker->words(mt_rand(2, 4), true))
->setName($this->faker->words(random_int(2, 4), true))
->setType($this->getReference(LoadEventTypes::$refs[array_rand(LoadEventTypes::$refs)], EventType::class))
->setCenter($center)
->setCircle(
@@ -79,7 +78,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
/** @var Person $person */
foreach ($people as $person) {
$nb = mt_rand(0, 3);
$nb = random_int(0, 3);
for ($i = 0; $i < $nb; ++$i) {
$event = $events[array_rand($events)];

View File

@@ -52,7 +52,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
}
/** (non-PHPdoc).
* @see PrependExtensionInterface::prepend()
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prepend(ContainerBuilder $container): void
{

View File

@@ -14,7 +14,6 @@ namespace Chill\EventBundle\Security\Authorization;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
@@ -27,9 +26,8 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
private readonly EventRepository $repository,
Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) {
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -189,14 +189,14 @@ crud:
title_edit: Rapport "belemmering" bewerken
title_delete: Belemmering verwijderen
button_delete: Verwijderen
confirm_message_delete: "%as_string% verwijderen?"
confirm_message_delete: %as_string% verwijderen?
cscv:
title_new: Nieuw CV voor %person%
title_view: CV voor %person%
title_edit: CV bewerken
title_delete: CV verwijderen
button_delete: Verwijderen
confirm_message_delete: "%as_string% verwijderen?"
confirm_message_delete: %as_string% verwijderen?
no_date: Geen datum aangegeven
no_end_date: einddatum onbekend
no_start_date: startdatum onbekend
@@ -206,7 +206,7 @@ crud:
title_edit: Immersie bewerken
title_delete: Immersie verwijderen
button_delete: Verwijderen
confirm_message_delete: "%as_string% verwijderen?"
confirm_message_delete: %as_string% verwijderen?
projet_prof:
title_new: Nieuw professioneel project voor %person%
title_view: Professioneel project voor %person%

View File

@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Action\User\UpdateProfile;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use libphonenumber\PhoneNumber;
use Symfony\Component\Validator\Constraints as Assert;
@@ -21,7 +22,7 @@ final class UpdateProfileCommand
public array $notificationFlags = [];
public function __construct(
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
#[PhonenumberConstraint]
public ?PhoneNumber $phonenumber,
#[Assert\Choice(choices: ['fr', 'nl'], message: 'Locale must be either "fr" or "nl"')]
public string $locale = 'fr',

View File

@@ -31,8 +31,7 @@ class LoadAddressesFRFromBANCommand extends Command
{
$this->setName('chill:main:address-ref-from-ban')
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers')
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send')
->addOption('allow-remove-double-refid', 'd', InputOption::VALUE_NONE, 'Should the address importer be allowed to remove same refid in the source data, if any');
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -41,7 +40,7 @@ class LoadAddressesFRFromBANCommand extends Command
foreach ($input->getArgument('departementNo') as $departementNo) {
$output->writeln('Import addresses for '.$departementNo);
$this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null, allowRemoveDoubleRefId: $input->hasOption('allow-remove-double-refid') ? $input->getOption('allow-remove-double-refid') : false);
$this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null);
}
return Command::SUCCESS;

View File

@@ -48,7 +48,7 @@ class LoadAndUpdateLanguagesCommand extends Command
/**
* (non-PHPdoc).
*
* @see Command::configure()
* @see \Symfony\Component\Console\Command\Command::configure()
*/
protected function configure()
{
@@ -73,7 +73,7 @@ class LoadAndUpdateLanguagesCommand extends Command
/**
* (non-PHPdoc).
*
* @see Command::execute()
* @see \Symfony\Component\Console\Command\Command::execute()
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{

View File

@@ -51,7 +51,7 @@ class LoadCountriesCommand extends Command
/**
* (non-PHPdoc).
*
* @see Command::configure()
* @see \Symfony\Component\Console\Command\Command::configure()
*/
protected function configure()
{
@@ -61,7 +61,7 @@ class LoadCountriesCommand extends Command
/**
* (non-PHPdoc).
*
* @see Command::execute()
* @see \Symfony\Component\Console\Command\Command::execute()
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{

View File

@@ -79,7 +79,5 @@ final class PostalCodeAPIController extends ApiController
$qb->andWhere('e.origin = :zero')
->setParameter('zero', 0);
$qb->andWhere('e.deletedAt IS NULL');
}
}

View File

@@ -62,15 +62,15 @@ final readonly class WorkflowViewSendPublicController
);
}
if (30 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'30 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('30 views reached, not allowed to see it again');
if (100 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
}
try {
$metadata = new EntityWorkflowViewMetadataDTO(
$workflowSend->getViews()->count(),
30 - $workflowSend->getViews()->count(),
100 - $workflowSend->getViews()->count(),
);
$response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),

View File

@@ -31,7 +31,6 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
public function __construct()
{
mt_srand(123456789);
$this->faker = \Faker\Factory::create('fr_FR');
}
@@ -68,7 +67,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
$ar->setRefId($this->faker->numerify('ref-id-######'));
$ar->setStreet($this->faker->streetName);
$ar->setStreetNumber((string) mt_rand(0, 199));
$ar->setStreetNumber((string) random_int(0, 199));
$ar->setPoint($this->getRandomPoint());
$ar->setPostcode($this->getReference(
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)],
@@ -89,8 +88,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
{
$lonBrussels = 4.35243;
$latBrussels = 50.84676;
$lon = $lonBrussels + 0.01 * mt_rand(-5, 5);
$lat = $latBrussels + 0.01 * mt_rand(-5, 5);
$lon = $lonBrussels + 0.01 * random_int(-5, 5);
$lat = $latBrussels + 0.01 * random_int(-5, 5);
return Point::fromLonLat($lon, $lat);
}

View File

@@ -84,7 +84,6 @@ use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
use Ramsey\Uuid\Doctrine\UuidType;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -211,15 +210,6 @@ class ChillMainExtension extends Extension implements
$config['top_banner'] ?? []
);
if (!in_array($config['homepage']['default_tab'], $config['homepage']['display_tabs'], true)) {
throw new InvalidConfigurationException('The chill_main.homepage.default_tab must be included in chill_main.homepage.display_tabs');
}
$container->setParameter(
'chill_main.homepage',
$config['homepage']
);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/doctrine.yaml');
@@ -251,7 +241,7 @@ class ChillMainExtension extends Extension implements
// $this->configureSms($config['short_messages'], $container, $loader);
}
public function prepend(ContainerBuilder $container): void
public function prepend(ContainerBuilder $container)
{
$this->prependNotifierTexterWithLegacyData($container);
@@ -266,7 +256,6 @@ class ChillMainExtension extends Extension implements
'available_languages' => $config['available_languages'],
'add_address' => $config['add_address'],
'chill_main_config' => $config,
'homepage_widget_config' => $config['homepage'],
],
'form_themes' => ['@ChillMain/Form/fields.html.twig'],
];

View File

@@ -20,7 +20,7 @@ class SearchableServicesCompilerPass implements CompilerPassInterface
/**
* (non-PHPdoc).
*
* @see CompilerPassInterface::process()
* @see \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process()
*/
public function process(ContainerBuilder $container)
{

View File

@@ -325,17 +325,6 @@ class Configuration implements ConfigurationInterface
->end()
->end();
/* @phpstan-ignore-next-line */
$rootNode->children()
->arrayNode('homepage')->addDefaultsIfNotSet()
->children()
->scalarNode('default_tab')->defaultValue('MyCustoms')->end()
->arrayNode('display_tabs')
->info('List of tabs to display on the homepage.')
->defaultValue(['MyCustoms', 'MyNotifications', 'MyAccompanyingCourses', 'MyEvaluations', 'MyTasks', 'MyWorkflows'])
->scalarPrototype()->end()
->end();
return $treeBuilder;
}
}

View File

@@ -32,7 +32,7 @@ abstract class AbstractWidgetFactory implements WidgetFactoryInterface
* Will create the definition by returning the definition from the `services.yml`
* file (or `services.xml` or `what-you-want.yml`).
*
* @see WidgetFactoryInterface::createDefinition()
* @see \Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface::createDefinition()
*/
public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config)
{

View File

@@ -45,9 +45,6 @@ class Center implements HasCenterInterface, \Stringable
#[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')]
private Collection $regroupments;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
private string $externalId = '';
/**
* Center constructor.
*/
@@ -127,19 +124,4 @@ class Center implements HasCenterInterface, \Stringable
return $this;
}
public function getExternalId(): string
{
return $this->externalId;
}
public function setExternalId(string $externalId): void
{
$this->externalId = $externalId;
}
public function hasExternalId(): bool
{
return '' !== $this->externalId;
}
}

View File

@@ -215,21 +215,17 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
/**
* @return list<User|UserGroup>
*/
public function getAllAddressees(): array
{
$allUsers = [];
foreach ($this->getAddressees() as $user) {
$allUsers['u_'.$user->getId()] = $user;
$allUsers[$user->getId()] = $user;
}
foreach ($this->getAddresseeUserGroups() as $userGroup) {
$allUsers['ug_'.$userGroup->getId()] = $userGroup;
foreach ($userGroup->getUsers() as $user) {
$allUsers['u_'.$user->getId()] = $user;
$allUsers[$user->getId()] = $user;
}
}

View File

@@ -215,14 +215,4 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
return $this;
}
public function isDeleted(): bool
{
return null !== $this->deletedAt;
}
public function getDeletedAt(): ?\DateTimeImmutable
{
return $this->deletedAt;
}
}

View File

@@ -23,6 +23,7 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -133,9 +134,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])]
private string $locale = 'fr';
#[ORM\ManyToMany(targetEntity: UserGroup::class, mappedBy: 'users')]
private Collection&Selectable $groupsAsMember;
/**
* User constructor.
*/
@@ -144,7 +142,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
$this->groupCenters = new ArrayCollection();
$this->scopeHistories = new ArrayCollection();
$this->jobHistories = new ArrayCollection();
$this->groupsAsMember = new ArrayCollection();
}
public function __toString(): string
@@ -174,32 +171,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->absenceEnd;
}
public function addGroupAsMember(UserGroup $userGroup): self
{
if (!$this->groupsAsMember->contains($userGroup)) {
$this->groupsAsMember->add($userGroup);
}
return $this;
}
public function removeGroupAsMember(UserGroup $userGroup): self
{
if ($this->groupsAsMember->contains($userGroup)) {
$this->groupsAsMember->removeElement($userGroup);
}
return $this;
}
/**
* @return Selectable&Collection<int, UserGroup>
*/
public function getGroupsAsMember(): Collection&Selectable
{
return $this->groupsAsMember;
}
/**
* Get attributes.
*
@@ -687,11 +658,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return true;
}
public function isUserGroup(): bool
{
return false;
}
private function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];

View File

@@ -54,7 +54,7 @@ class UserGroup
/**
* @var Collection<int, User>&Selectable<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'groupsAsMember')]
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
private Collection&Selectable $users;
@@ -129,7 +129,6 @@ class UserGroup
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
$user->addGroupAsMember($this);
}
return $this;
@@ -139,7 +138,6 @@ class UserGroup
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
$user->removeGroupAsMember($this);
}
return $this;
@@ -258,21 +256,6 @@ class UserGroup
return true;
}
public function isUser(): bool
{
return false;
}
/**
* Return a locale for the userGroup.
*
* Currently hardcoded, should be replaced by a property.
*/
public function getLocale(): string
{
return 'fr';
}
public function contains(User $user): bool
{
return $this->users->contains($user);

View File

@@ -394,10 +394,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function isUserInvolved(User $user): bool
{
if ($this->getCreatedBy() === $user) {
return true;
}
foreach ($this->getSteps() as $step) {
if ($step->getAllDestUser()->contains($user)) {
return true;

View File

@@ -14,8 +14,7 @@ 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\UserGroupRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
@@ -25,8 +24,7 @@ readonly class SendImmediateNotificationEmailHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepositoryInterface $userRepository,
private UserGroupRepository $userGroupRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
@@ -38,13 +36,7 @@ readonly class SendImmediateNotificationEmailHandler
public function __invoke(SendImmediateNotificationEmailMessage $message): void
{
$notification = $this->notificationRepository->find($message->getNotificationId());
if (null !== $message->getUserId()) {
$addressee = $this->userRepository->find($message->getUserId());
} elseif (null !== $message->getUserGroupId()) {
$addressee = $this->userGroupRepository->find($message->getUserGroupId());
} else {
throw new \InvalidArgumentException('Addressee not found: nor an user nor a user group');
}
$addressee = $this->userRepository->find($message->getAddresseeId());
if (null === $notification) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
@@ -56,11 +48,10 @@ readonly class SendImmediateNotificationEmailHandler
if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
'user_id' => $message->getUserId(),
'user_group_id' => $message->getUserGroupId(),
'addressee_id' => $message->getAddresseeId(),
]);
throw new \InvalidArgumentException(sprintf('User with ID %s or user group with id %s not found', $message->getUserId(), $message->getUserGroupId()));
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
}
try {
@@ -68,8 +59,7 @@ readonly class SendImmediateNotificationEmailHandler
} catch (\Exception $e) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
'notification_id' => $message->getNotificationId(),
'user_id' => $message->getUserId(),
'user_group_id' => $message->getUserGroupId(),
'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