mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Compare commits
	
		
			132 Commits
		
	
	
		
			2.15.2
			...
			270-ne-pas
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8516e89c9c | |||
| 4091efc72e | |||
| b577bd7123 | |||
| dbb9feb129 | |||
| e8b95f8491 | |||
| 8de37a9ef3 | |||
| 8fd6986c47 | |||
| 807f1b4aa1 | |||
| f3002631ea | |||
| 9e667d4de4 | |||
| fc88a5f40d | |||
| 9ff7aef3fc | |||
| 4f08019618 | |||
| 2a58330832 | |||
| a2cea3df02 | |||
| 9ac43ecf5b | |||
| f78f5e8419 | |||
| ccf3324bc2 | |||
| dfe780f0f5 | |||
| dd056efa0d | |||
| 18c0b6a47f | |||
| df0afcd228 | |||
| d66933c8b5 | |||
| 0ff51b0a5c | |||
| d7f4895248 | |||
| 7aee722957 | |||
| 5880858191 | |||
| 96105b101f | |||
| d29415317b | |||
| 2ad3bbe96f | |||
| 1d636f5e9e | |||
| f0dbb17172 | |||
| f1dbc17dad | |||
| 09578a775c | |||
| c888b5b84f | |||
| 27d76d9579 | |||
| 5b714f17be | |||
| bbb167bb85 | |||
| d713087dcb | |||
| 569aeeef87 | |||
| 97f2c75de8 | |||
| 4a2078dc65 | |||
| 00444e1e56 | |||
| f02c5bca13 | |||
| 0d56828ebd | |||
| 8b28667fe5 | |||
| 72f73ec8e7 | |||
| b3d1320c94 | |||
| 2ed42e1a2c | |||
| d0e5ba16fe | |||
| 8e65ad9476 | |||
| cf7338b690 | |||
| 63dd71037a | |||
| cc281762b3 | |||
| aa0cadfa84 | |||
| 6e2cce9531 | |||
| 1fbbf2b2ad | |||
| e586b8ee5e | |||
| 6d04e477f8 | |||
| 6b7b2ae522 | |||
| 9b9c2774ad | |||
| e902b6d409 | |||
| d8bf6a195f | |||
| 7c3152f277 | |||
| cef218fed5 | |||
| 930a76cc66 | |||
| f11f7498d7 | |||
| 1a9af6b0b1 | |||
| d347f6ae60 | |||
| 3bb911b4d0 | |||
| f00b39980c | |||
| 09882bb4be | |||
| 1d21499eab | |||
| 8ef001e67e | |||
| 458df45fa5 | |||
| 2b968b9a5b | |||
| f1fa4d415e | |||
| 2312a8d46f | |||
| 67c3de733f | |||
| c05451bfe9 | |||
| 8be9fb6553 | |||
| f5f6eb78a2 | |||
| 7a7e66146b | |||
| 4bbad4fc61 | |||
| 86613a9be9 | |||
| 21bd6478ad | |||
| 5849d8d670 | |||
| 568ee079b5 | |||
| bf97b2a50c | |||
| 01785ed494 | |||
| 97d401b7f6 | |||
| 44ccfe92b6 | |||
| b6ea857389 | |||
| f8840d89bf | |||
| 813f2f1e12 | |||
| 4a15a89102 | |||
| c707a34f16 | |||
| 0c9010f065 | |||
| 3871299346 | |||
| e2e0b08210 | |||
| 4df0542932 | |||
| 13854e59de | |||
| 574ad42a76 | |||
| 4736fca679 | |||
| 32ae2f8f0d | |||
| d58c0a867d | |||
| 15f8432ce0 | |||
| ae7637acc6 | |||
| ce391a6de8 | |||
| 950835c10b | |||
| 9ba557a5bf | |||
| 439fecd69f | |||
| f02168950f | |||
| 58c2235b88 | |||
| 42c5577027 | |||
| 036fe8d6f8 | |||
| 51ebc253aa | |||
| 4fdc7fd210 | |||
| 0bf6c07e8d | |||
| 7a12602699 | |||
| 15a927a9f8 | |||
| 0a2805f23f | |||
| 27ce322690 | |||
| 469e379166 | |||
| f103b228e4 | |||
| d2a31de1be | |||
| 138a537d2b | |||
| c06c861e17 | |||
| 34cbd2605c | |||
| 044bab45ad | |||
| b9890d1302 | |||
| 5b2a2a1bc5 | 
							
								
								
									
										15
									
								
								.changes/v2.16.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.changes/v2.16.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| ## v2.16.0 - 2024-02-08 | ||||
| ### Feature | ||||
| * ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span  | ||||
| * ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids  | ||||
| * ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this  | ||||
| * ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating  | ||||
| * ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address  | ||||
| * ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason  | ||||
| * ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work  | ||||
| * Modernize the event bundle, with some new fields and multiple improvements  | ||||
| ### Fixed | ||||
| * ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method  | ||||
| * ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form  | ||||
| ### UX | ||||
| * ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.  | ||||
							
								
								
									
										3
									
								
								.changes/v2.16.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.16.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.16.1 - 2024-02-09 | ||||
| ### Fixed | ||||
| * Force bootstrap version to avoid error in builds with newer version  | ||||
							
								
								
									
										3
									
								
								.changes/v2.16.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.16.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.16.2 - 2024-02-21 | ||||
| ### Fixed | ||||
| * Check for null values in closing motive of parcours d'accompagnement for correct rendering of template  | ||||
							
								
								
									
										5
									
								
								.changes/v2.16.3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/v2.16.3.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| ## v2.16.3 - 2024-02-26 | ||||
| ### Fixed | ||||
| * ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'  | ||||
| ### UX | ||||
| * ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters  | ||||
							
								
								
									
										9
									
								
								.changes/v2.17.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.changes/v2.17.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| ## v2.17.0 - 2024-03-19 | ||||
| ### Feature | ||||
| * ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates  | ||||
| * ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course  | ||||
| * ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields  | ||||
| * ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage | ||||
| ### Fixed | ||||
| * ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill  | ||||
| * ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period  | ||||
							
								
								
									
										5
									
								
								.changes/v2.18.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/v2.18.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| ## v2.18.0 - 2024-03-26 | ||||
| ### Feature | ||||
| * ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation  | ||||
| ### Fixed | ||||
| * ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job  | ||||
							
								
								
									
										3
									
								
								.changes/v2.18.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.18.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.18.1 - 2024-03-26 | ||||
| ### Fixed | ||||
| * Fix layout issue in document generation for admin (minor)  | ||||
							
								
								
									
										3
									
								
								.changes/v2.18.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.18.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.18.2 - 2024-04-12 | ||||
| ### Fixed | ||||
| * ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record  | ||||
| @@ -23,3 +23,7 @@ max_line_length = 0 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
|  | ||||
| [.rst] | ||||
| ident_size = 3 | ||||
| ident_style = space | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,8 @@ variables: | ||||
|     DEFAULT_CARRIER_CODE: BE | ||||
|     # force a timezone | ||||
|     TZ: Europe/Brussels | ||||
|     # avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations | ||||
|     SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1 | ||||
|  | ||||
| stages: | ||||
|     - Composer install | ||||
|   | ||||
							
								
								
									
										54
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,6 +6,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v2.18.2 - 2024-04-12 | ||||
| ### Fixed | ||||
| * ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record  | ||||
|  | ||||
| ## v2.18.1 - 2024-03-26 | ||||
| ### Fixed | ||||
| * Fix layout issue in document generation for admin (minor)  | ||||
|  | ||||
| ## v2.18.0 - 2024-03-26 | ||||
| ### Feature | ||||
| * ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation  | ||||
| ### Fixed | ||||
| * ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job  | ||||
|  | ||||
| ## v2.17.0 - 2024-03-19 | ||||
| ### Feature | ||||
| * ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates  | ||||
| * ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course  | ||||
| * ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields  | ||||
| * ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage | ||||
| ### Fixed | ||||
| * ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill  | ||||
| * ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period  | ||||
|  | ||||
| ## v2.16.3 - 2024-02-26 | ||||
| ### Fixed | ||||
| * ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'  | ||||
| ### UX | ||||
| * ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters  | ||||
|  | ||||
| ## v2.16.2 - 2024-02-21 | ||||
| ### Fixed | ||||
| * Check for null values in closing motive of parcours d'accompagnement for correct rendering of template  | ||||
|  | ||||
| ## v2.16.1 - 2024-02-09 | ||||
| ### Fixed | ||||
| * Force bootstrap version to avoid error in builds with newer version  | ||||
|  | ||||
| ## v2.16.0 - 2024-02-08 | ||||
| ### Feature | ||||
| * ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span  | ||||
| * ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids  | ||||
| * ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this  | ||||
| * ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating  | ||||
| * ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address  | ||||
| * ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason  | ||||
| * ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work  | ||||
| * Modernize the event bundle, with some new fields and multiple improvements  | ||||
| ### Fixed | ||||
| * ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method  | ||||
| * ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form  | ||||
| ### UX | ||||
| * ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.  | ||||
|  | ||||
| ## v2.15.2 - 2024-01-11 | ||||
| ### Fixed | ||||
| * Fix the id_seq used when creating a new accompanying period participation during fusion of two person files  | ||||
|   | ||||
| @@ -242,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati | ||||
|    Continue to explain the export framework | ||||
|  | ||||
| .. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main | ||||
|  | ||||
|  | ||||
| With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ? | ||||
| `````````````````````````````````````````````````````````````````````````````````````````````````````` | ||||
|  | ||||
| As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course | ||||
| which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on | ||||
| two accompanying period only: the ones with id 329 and 334. | ||||
|  | ||||
| Let's see the list of activities associated with those accompanying period: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7 | ||||
|        ORDER BY accompanyingperiod_id; | ||||
|  | ||||
| We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one. | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: id, accompanyingperiod_id, type_id | ||||
|  | ||||
|    990,329,7 | ||||
|    986,329,7 | ||||
|    987,329,7 | ||||
|    993,329,7 | ||||
|    991,329,7 | ||||
|    992,329,7 | ||||
|    1000,334,7 | ||||
|  | ||||
| Let's calculate the average duration for those accompanying periods, and the number of period: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334); | ||||
|  | ||||
| The result of this query is: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: AVG, COUNT | ||||
|  | ||||
|    2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2 | ||||
|  | ||||
| Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep | ||||
| only the accompanying period which contains the given activity type: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|                  JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|                  WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| What are the results here ? | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    7 | ||||
|  | ||||
| :code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which | ||||
| are taken into account for the computation: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period | ||||
|    JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: accompanyingperiod.id, activity.id | ||||
|  | ||||
|    329,993 | ||||
|    334,1000 | ||||
|    329,987 | ||||
|    329,990 | ||||
|    329,991 | ||||
|    329,992 | ||||
|    329,986 | ||||
|  | ||||
| For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we | ||||
| count one entry for each activity (actually, we count the number of activities). | ||||
|  | ||||
| So, let's use the :code:`DISTINCT` keyword to count only once the equal ids: | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|    SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|    JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| Now, it works again... | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    2 | ||||
|  | ||||
| But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and | ||||
| :code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result. | ||||
|  | ||||
| The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); | ||||
|  | ||||
| The result is correct without :code:`DISTINCT` keyword: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    2 | ||||
|  | ||||
| And we can now compute the average duration without fear: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|   SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period | ||||
|   WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); | ||||
|  | ||||
| Give the result: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: AVG | ||||
|  | ||||
|    2 years 2 mons 21 days 12 hours 0 mins 0.0 secs | ||||
|   | ||||
| @@ -48,7 +48,7 @@ Clone or download the chill-skeleton project and `cd` into the main directory. | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    git clone https://gitlab.com/Chill-Projet/chill-skeleton-basic.git | ||||
|    git clone https://gitea.champs-libres.be/Chill-project/chill-skeleton-basic.git | ||||
|    cd chill-skeleton-basic | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,16 @@ Chill can store a list of geolocated address references, which are used to sugge | ||||
|  | ||||
| Those addresses may be load from a dedicated source. | ||||
|  | ||||
| Countries | ||||
| ========= | ||||
|  | ||||
| In order to load addresses into the chill application we first have to make sure that a list of countries is present. | ||||
| To import the countries run the following command. | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|     bin/console chill:main:countries:populate | ||||
|  | ||||
| In France | ||||
| ========= | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,9 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt | ||||
| These are alias conventions : | ||||
|  | ||||
| | Entity                                  | Join                                    | Attribute                                  | Alias                                      | | ||||
| |:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------| | ||||
| |:----------------------------------------|:----------------------------------------|:-------------------------------------------|:-------------------------------------------| | ||||
| | AccompanyingPeriodStepHistory::class    |                                         |                                            | acpstephistory (contexte ACP_STEP_HISTORY) | | ||||
| |                                         | AccompanyingPeriod::class               | acpstephistory.period                      | acp                                        | | ||||
| | AccompanyingPeriod::class               |                                         |                                            | acp                                        | | ||||
| |                                         | AccompanyingPeriodWork::class           | acp.works                                  | acpw                                       | | ||||
| |                                         | AccompanyingPeriodParticipation::class  | acp.participations                         | acppart                                    | | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|     "@symfony/webpack-encore": "^4.1.0", | ||||
|     "@tsconfig/node14": "^1.0.1", | ||||
|     "bindings": "^1.5.0", | ||||
|     "bootstrap": "^5.0.1", | ||||
|     "bootstrap": "5.2.3", | ||||
|     "chokidar": "^3.5.1", | ||||
|     "fork-awesome": "^1.1.7", | ||||
|     "jquery": "^3.6.0", | ||||
|   | ||||
| @@ -291,7 +291,11 @@ class ActivityType | ||||
|     public function checkSocialActionsVisibility(ExecutionContextInterface $context, mixed $payload) | ||||
|     { | ||||
|         if ($this->socialIssuesVisible !== $this->socialActionsVisible) { | ||||
|             if (!(2 === $this->socialIssuesVisible && 1 === $this->socialActionsVisible)) { | ||||
|             // if social issues are invisible then social actions cannot be optional or required + if social issues are optional then social actions shouldn't be required | ||||
|             if ( | ||||
|                 (0 === $this->socialIssuesVisible && (1 === $this->socialActionsVisible || 2 === $this->socialActionsVisible)) | ||||
|                 || (1 === $this->socialIssuesVisible && 2 === $this->socialActionsVisible) | ||||
|             ) { | ||||
|                 $context | ||||
|                     ->buildViolation('The socialActionsVisible value is not compatible with the socialIssuesVisible value') | ||||
|                     ->atPath('socialActionsVisible') | ||||
|   | ||||
| @@ -57,7 +57,7 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface | ||||
|  | ||||
|     public function getLabels($key, array $values, mixed $data) | ||||
|     { | ||||
|         return function (null|int|string $value): string { | ||||
|         return function (int|string|null $value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'export.aggregator.acp.by_activity_type.activity_type'; | ||||
|             } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ final readonly class ActivityPresenceAggregator implements AggregatorInterface | ||||
|  | ||||
|     public function getLabels($key, array $values, mixed $data) | ||||
|     { | ||||
|         return function (null|int|string $value): string { | ||||
|         return function (int|string|null $value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'export.aggregator.activity.by_activity_presence.header'; | ||||
|             } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ declare(strict_types=1); | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
| 
 | ||||
| namespace Chill\ActivityBundle\Export\Aggregator\PersonAggregators; | ||||
| namespace Chill\ActivityBundle\Export\Aggregator; | ||||
| 
 | ||||
| use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\ActivityBundle\Repository\ActivityReasonCategoryRepository; | ||||
| @@ -25,8 +25,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||||
| 
 | ||||
| class ActivityReasonAggregator implements AggregatorInterface, ExportElementValidatedInterface | ||||
| { | ||||
|     public function __construct(protected ActivityReasonCategoryRepository $activityReasonCategoryRepository, protected ActivityReasonRepository $activityReasonRepository, protected TranslatableStringHelper $translatableStringHelper) | ||||
|     { | ||||
|     public function __construct( | ||||
|         protected ActivityReasonCategoryRepository $activityReasonCategoryRepository, | ||||
|         protected ActivityReasonRepository $activityReasonRepository, | ||||
|         protected TranslatableStringHelper $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
| 
 | ||||
|     public function addRole(): ?string | ||||
| @@ -51,7 +54,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali | ||||
| 
 | ||||
|         // make a jointure only if needed
 | ||||
|         if (!\in_array('actreasons', $qb->getAllAliases(), true)) { | ||||
|             $qb->innerJoin('activity.reasons', 'actreasons'); | ||||
|             $qb->leftJoin('activity.reasons', 'actreasons'); | ||||
|         } | ||||
| 
 | ||||
|         // join category if necessary
 | ||||
| @@ -62,19 +65,12 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // add the "group by" part
 | ||||
|         $groupBy = $qb->getDQLPart('groupBy'); | ||||
| 
 | ||||
|         if (\count($groupBy) > 0) { | ||||
|         $qb->addGroupBy($alias); | ||||
|         } else { | ||||
|             $qb->groupBy($alias); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function applyOn(): string | ||||
|     { | ||||
|         return Declarations::ACTIVITY_PERSON; | ||||
|         return Declarations::ACTIVITY; | ||||
|     } | ||||
| 
 | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
| @@ -96,7 +92,9 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali | ||||
| 
 | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|         return [ | ||||
|             'level' => 'reasons', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     public function getLabels($key, array $values, $data) | ||||
| @@ -58,7 +58,7 @@ class ActivityTypeAggregator implements AggregatorInterface | ||||
|  | ||||
|     public function getLabels($key, array $values, $data): \Closure | ||||
|     { | ||||
|         return function (null|int|string $value): string { | ||||
|         return function (int|string|null $value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'Activity type'; | ||||
|             } | ||||
|   | ||||
| @@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface | ||||
|     { | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'choices' => $this->userJobRepository->findAllOrderedByName(), | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'class' => UserJob::class, | ||||
|                 'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getLabel() | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User\UserScopeHistory; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface | ||||
|     private const PREFIX = 'acp_act_filter_creator_scope'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper, | ||||
|         private readonly ScopeRepositoryInterface $scopeRepository, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -76,6 +78,7 @@ class CreatorScopeFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('scopes', EntityType::class, [ | ||||
|                 'class' => Scope::class, | ||||
|                 'choices' => $this->scopeRepository->findAllActive(), | ||||
|                 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getName() | ||||
|                 ), | ||||
|   | ||||
| @@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -27,7 +28,8 @@ class UsersJobFilter implements FilterInterface | ||||
|     private const PREFIX = 'act_filter_user_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly UserJobRepositoryInterface $userJobRepository | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -69,6 +71,7 @@ class UsersJobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), | ||||
|                 'multiple' => true, | ||||
|                 'expanded' => true, | ||||
|   | ||||
| @@ -95,7 +95,7 @@ class ActivityType extends AbstractType | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */ | ||||
|         /** @var AccompanyingPeriod|null $accompanyingPeriod */ | ||||
|         $accompanyingPeriod = null; | ||||
|  | ||||
|         if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) { | ||||
|   | ||||
| @@ -243,7 +243,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos | ||||
|             thirdparties.thirdpartyids, | ||||
|             persons.personids, | ||||
|             actions.socialactionids, | ||||
|             issues.socialissueids | ||||
|             issues.socialissueids, | ||||
|             a.user_id | ||||
|  | ||||
|         FROM activity a | ||||
|         LEFT JOIN chill_main_location location ON a.location_id = location.id | ||||
| @@ -283,6 +284,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos | ||||
|             ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') | ||||
|             ->addFieldResult('activityPresence', 'presence_id', 'id') | ||||
|             ->addFieldResult('activityPresence', 'presence_name', 'name') | ||||
|             ->addScalarResult('user_id', 'userId', Types::INTEGER) | ||||
|  | ||||
|             // results which cannot be mapped into entity | ||||
|             ->addScalarResult('comment_comment', 'comment', Types::TEXT) | ||||
|   | ||||
| @@ -36,14 +36,14 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQueryInterface | ||||
|     public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface | ||||
|     { | ||||
|         $query = $this->buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content); | ||||
|  | ||||
|         return $this->addFetchQueryByPersonACL($query, $person); | ||||
|     } | ||||
|  | ||||
|     public function buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery | ||||
|     public function buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|         $activityMetadata = $this->em->getClassMetadata(Activity::class); | ||||
| @@ -72,7 +72,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum | ||||
|         return $this->addWhereClauses($query, $startDate, $endDate, $content); | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery | ||||
|     public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|         $activityMetadata = $this->em->getClassMetadata(Activity::class); | ||||
| @@ -123,7 +123,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum | ||||
|         return $this->addWhereClauses($query, $startDate, $endDate, $content); | ||||
|     } | ||||
|  | ||||
|     private function addWhereClauses(FetchQuery $query, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery | ||||
|     private function addWhereClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|  | ||||
|   | ||||
| @@ -25,12 +25,12 @@ interface ActivityDocumentACLAwareRepositoryInterface | ||||
|      * | ||||
|      * This method must check the rights to see a document: the user must be allowed to see the given activities | ||||
|      */ | ||||
|     public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQueryInterface; | ||||
|     public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface; | ||||
|  | ||||
|     /** | ||||
|      * Return a fetch query for querying document's activities for an activity in accompanying periods, but for a given person. | ||||
|      * | ||||
|      * This method must check the rights to see a document: the user must be allowed to see the given accompanying periods | ||||
|      */ | ||||
|     public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery; | ||||
|     public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery; | ||||
| } | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class ActivityPresenceRepository implements ActivityPresenceRepositoryInterface | ||||
|         return $this->repository->findAll(); | ||||
|     } | ||||
|  | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ interface ActivityPresenceRepositoryInterface | ||||
|     /** | ||||
|      * @return array|ActivityPresence[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array; | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?ActivityPresence; | ||||
|  | ||||
|   | ||||
| @@ -48,7 +48,7 @@ final class ActivityTypeRepository implements ActivityTypeRepositoryInterface | ||||
|     /** | ||||
|      * @return array|ActivityType[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Service\DocGenerator; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityPresence; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; | ||||
| @@ -112,7 +113,7 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list | ||||
|      * @return list<Activity> | ||||
|      */ | ||||
|     private function filterActivitiesByUser(array $activities, User $user): array | ||||
|     { | ||||
| @@ -120,6 +121,12 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|             array_filter( | ||||
|                 $activities, | ||||
|                 function ($activity) use ($user) { | ||||
|                     $u = $activity['user']; | ||||
|  | ||||
|                     if (null !== $u && $u['username'] === $user->getUsername()) { | ||||
|                         return true; | ||||
|                     } | ||||
|  | ||||
|                     $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); | ||||
|  | ||||
|                     return \in_array($user->getUsername(), $activityUsernames, true); | ||||
| @@ -129,7 +136,7 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list | ||||
|      * @return list<AccompanyingPeriod\AccompanyingPeriodWork> | ||||
|      */ | ||||
|     private function filterWorksByUser(array $works, User $user): array | ||||
|     { | ||||
| @@ -216,6 +223,15 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|         foreach ($activities as $row) { | ||||
|             $activity = $row[0]; | ||||
|  | ||||
|             $user = match (null === $row['userId']) { | ||||
|                 false => $this->userRepository->find($row['userId']), | ||||
|                 true => null, | ||||
|             }; | ||||
|  | ||||
|             $activity['user'] = $this->normalizer->normalize($user, 'docgen', [ | ||||
|                 AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class, | ||||
|             ]); | ||||
|  | ||||
|             $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [ | ||||
|                 AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class, | ||||
|             ]); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|         $activityMetadata = $this->em->getClassMetadata(Activity::class); | ||||
| @@ -100,7 +100,7 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen | ||||
|         return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person); | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         return $this->activityDocumentACLAwareRepository | ||||
|             ->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content); | ||||
|   | ||||
| @@ -28,7 +28,7 @@ final readonly class PersonActivityGenericDocProvider implements GenericDocForPe | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         return $this->personActivityDocumentACLAwareRepository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext( | ||||
|             $person, | ||||
|   | ||||
| @@ -9,10 +9,10 @@ declare(strict_types=1); | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
| 
 | ||||
| namespace Chill\ActivityBundle\Tests\Export\Aggregator\PersonAggregators; | ||||
| namespace Chill\ActivityBundle\Tests\Export\Aggregator; | ||||
| 
 | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator; | ||||
| use Chill\ActivityBundle\Export\Aggregator\ActivityReasonAggregator; | ||||
| use Chill\MainBundle\Test\Export\AbstractAggregatorTest; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| @@ -33,14 +33,14 @@ final class ActivityReasonAggregatorTest extends AbstractAggregatorTest | ||||
|         self::bootKernel(); | ||||
| 
 | ||||
|         $this->aggregator = self::$container->get(ActivityReasonAggregator::class); | ||||
| 
 | ||||
|         /* | ||||
|                 $request = $this->prophesize() | ||||
|                     ->willExtend(\Symfony\Component\HttpFoundation\Request::class); | ||||
| 
 | ||||
|                 $request->getLocale()->willReturn('fr'); | ||||
| 
 | ||||
|                 self::$container->get('request_stack') | ||||
|             ->push($request->reveal()); | ||||
|                     ->push($request->reveal());*/ | ||||
|     } | ||||
| 
 | ||||
|     public function getAggregator() | ||||
| @@ -65,7 +65,12 @@ final class ActivityReasonAggregatorTest extends AbstractAggregatorTest | ||||
|         return [ | ||||
|             $em->createQueryBuilder() | ||||
|                 ->select('count(activity.id)') | ||||
|                 ->from(Activity::class, 'activity'), | ||||
|                 ->from(Activity::class, 'activity') | ||||
|                 ->join('activity.person', 'person'), | ||||
|             $em->createQueryBuilder() | ||||
|                 ->select('count(activity.id)') | ||||
|                 ->from(Activity::class, 'activity') | ||||
|                 ->join('activity.accompanyingPeriod', 'accompanyingPeriod'), | ||||
|             $em->createQueryBuilder() | ||||
|                 ->select('count(activity.id)') | ||||
|                 ->from(Activity::class, 'activity') | ||||
| @@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testfindByAccompanyingPeriodSimplified(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByAccompanyingPeriodSimplified($period); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
| @@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException('no jobs found'); | ||||
|             $job = new UserJob(); | ||||
|             $job->setLabel(['fr' => 'test']); | ||||
|             $this->entityManager->persist($job); | ||||
|             $this->entityManager->flush(); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|   | ||||
| @@ -157,7 +157,7 @@ final class ActivityVoterTest extends KernelTestCase | ||||
|      * | ||||
|      * @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface | ||||
|      */ | ||||
|     protected function prepareToken(User $user = null) | ||||
|     protected function prepareToken(?User $user = null) | ||||
|     { | ||||
|         $token = $this->prophet->prophesize(); | ||||
|         $token | ||||
|   | ||||
| @@ -0,0 +1,139 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\ActivityBundle\Tests\Service\DocGenerator; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Service\DocGenerator\ListActivitiesByAccompanyingPeriodContext; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ListActivitiesByAccompanyingPeriodContextTest extends KernelTestCase | ||||
| { | ||||
|     private ListActivitiesByAccompanyingPeriodContext $listActivitiesByAccompanyingPeriodContext; | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|     private UserRepositoryInterface $userRepository; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class); | ||||
|         $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); | ||||
|         $this->userRepository = self::$container->get(UserRepositoryInterface::class); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void | ||||
|     { | ||||
|         $context = $this->getContext(); | ||||
|         $template = new DocGeneratorTemplate(); | ||||
|         $template->setOptions([ | ||||
|             'mainPerson' => false, | ||||
|             'person1' => false, | ||||
|             'person2' => false, | ||||
|             'thirdParty' => false, | ||||
|         ]); | ||||
|  | ||||
|         $data = $context->getData( | ||||
|             $template, | ||||
|             $this->accompanyingPeriodRepository->find($accompanyingPeriodId), | ||||
|             ['myActivitiesOnly' => false, 'myWorksOnly' => false] | ||||
|         ); | ||||
|  | ||||
|         self::assertIsArray($data); | ||||
|         self::assertArrayHasKey('activities', $data); | ||||
|         self::assertIsArray($data['activities']); | ||||
|         self::assertGreaterThan(0, count($data['activities'])); | ||||
|         self::assertIsArray($data['activities'][0]); | ||||
|         self::assertArrayHasKey('user', $data['activities'][0]); | ||||
|         self::assertIsArray($data['activities'][0]['user']); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void | ||||
|     { | ||||
|         $context = $this->getContext(); | ||||
|         $template = new DocGeneratorTemplate(); | ||||
|         $template->setOptions([ | ||||
|             'mainPerson' => false, | ||||
|             'person1' => false, | ||||
|             'person2' => false, | ||||
|             'thirdParty' => false, | ||||
|         ]); | ||||
|  | ||||
|         $data = $context->getData( | ||||
|             $template, | ||||
|             $this->accompanyingPeriodRepository->find($accompanyingPeriodId), | ||||
|             ['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)] | ||||
|         ); | ||||
|  | ||||
|         self::assertIsArray($data); | ||||
|         self::assertArrayHasKey('activities', $data); | ||||
|         self::assertIsArray($data['activities']); | ||||
|         self::assertGreaterThan(0, count($data['activities'])); | ||||
|         self::assertIsArray($data['activities'][0]); | ||||
|         self::assertArrayHasKey('user', $data['activities'][0]); | ||||
|         self::assertIsArray($data['activities'][0]['user']); | ||||
|     } | ||||
|  | ||||
|     public static function provideAccompanyingPeriod(): array | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $em = self::$container->get(EntityManagerInterface::class); | ||||
|  | ||||
|         if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException('no period found'); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException('no user found'); | ||||
|         } | ||||
|  | ||||
|         $activity = new Activity(); | ||||
|         $activity | ||||
|             ->setAccompanyingPeriod($period) | ||||
|             ->setUser($user) | ||||
|             ->setDate(new \DateTime()); | ||||
|  | ||||
|         $em->persist($activity); | ||||
|         $em->flush(); | ||||
|  | ||||
|         self::ensureKernelShutdown(); | ||||
|  | ||||
|         return [ | ||||
|             [$period->getId(), $user->getId()], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function getContext(): ListActivitiesByAccompanyingPeriodContext | ||||
|     { | ||||
|         return $this->listActivitiesByAccompanyingPeriodContext; | ||||
|     } | ||||
| } | ||||
| @@ -153,7 +153,7 @@ services: | ||||
|  | ||||
|  | ||||
|     ## Aggregators | ||||
|     Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator: | ||||
|     Chill\ActivityBundle\Export\Aggregator\ActivityReasonAggregator: | ||||
|         tags: | ||||
|             - { name: chill.export_aggregator, alias: activity_reason_aggregator } | ||||
|  | ||||
|   | ||||
| @@ -396,7 +396,7 @@ export: | ||||
|             by_creator_job: | ||||
|                 job_form_label: Métiers | ||||
|                 Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange | ||||
|                 'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%" | ||||
|                 'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%" | ||||
|             by_persons: | ||||
|                 Filter activity by persons: Filtrer les échanges par usager participant | ||||
|                 'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%' | ||||
|   | ||||
| @@ -49,7 +49,7 @@ final class AsideActivityController extends CRUDController | ||||
|         return $asideActivity; | ||||
|     } | ||||
|  | ||||
|     protected function buildQueryEntities(string $action, Request $request, FilterOrderHelper $filterOrder = null) | ||||
|     protected function buildQueryEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null) | ||||
|     { | ||||
|         $qb = parent::buildQueryEntities($action, $request); | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|      * | ||||
|      * @Assert\NotBlank | ||||
|      */ | ||||
|     private \Chill\MainBundle\Entity\User $agent; | ||||
|     private User $agent; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="datetime") | ||||
| @@ -44,7 +44,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|      * | ||||
|      * @ORM\JoinColumn(nullable=false) | ||||
|      */ | ||||
|     private \Chill\MainBundle\Entity\User $createdBy; | ||||
|     private User $createdBy; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="datetime") | ||||
| @@ -82,7 +82,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|      * | ||||
|      * @ORM\JoinColumn(nullable=false) | ||||
|      */ | ||||
|     private ?\Chill\AsideActivityBundle\Entity\AsideActivityCategory $type = null; | ||||
|     private ?AsideActivityCategory $type = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="datetime", nullable=true) | ||||
| @@ -92,7 +92,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|     /** | ||||
|      * @ORM\ManyToOne(targetEntity=User::class) | ||||
|      */ | ||||
|     private \Chill\MainBundle\Entity\User $updatedBy; | ||||
|     private User $updatedBy; | ||||
|  | ||||
|     public function getAgent(): ?User | ||||
|     { | ||||
|   | ||||
| @@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface | ||||
|     private const PREFIX = 'aside_act_filter_user_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly UserJobRepositoryInterface $userJobRepository | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -69,6 +71,7 @@ class ByUserJobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), | ||||
|                 'multiple' => true, | ||||
|                 'expanded' => true, | ||||
|   | ||||
| @@ -49,7 +49,7 @@ class AsideActivityCategoryRepository implements ObjectRepository | ||||
|      * | ||||
|      * @return AsideActivityCategory[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -132,14 +132,14 @@ abstract class AbstractElement | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setComment(string $comment = null): self | ||||
|     public function setComment(?string $comment = null): self | ||||
|     { | ||||
|         $this->comment = $comment; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setEndDate(\DateTimeInterface $endDate = null): self | ||||
|     public function setEndDate(?\DateTimeInterface $endDate = null): self | ||||
|     { | ||||
|         if ($endDate instanceof \DateTime) { | ||||
|             $this->endDate = \DateTimeImmutable::createFromMutable($endDate); | ||||
|   | ||||
| @@ -66,7 +66,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface | ||||
|      * | ||||
|      * @return array<ChargeKind> | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -36,7 +36,7 @@ interface ChargeKindRepositoryInterface extends ObjectRepository | ||||
|     /** | ||||
|      * @return array<ChargeKind> | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array; | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?ChargeKind; | ||||
|  | ||||
|   | ||||
| @@ -71,7 +71,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface | ||||
|      * | ||||
|      * @return list<ResourceKind> | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -36,7 +36,7 @@ interface ResourceKindRepositoryInterface extends ObjectRepository | ||||
|     /** | ||||
|      * @return list<ResourceKind> | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array; | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?ResourceKind; | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ final class Version20221207105407 extends AbstractMigration implements Container | ||||
|         return 'Use new budget admin entities'; | ||||
|     } | ||||
|  | ||||
|     public function setContainer(ContainerInterface $container = null) | ||||
|     public function setContainer(?ContainerInterface $container = null) | ||||
|     { | ||||
|         $this->container = $container; | ||||
|     } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ namespace Chill\CalendarBundle\Exception; | ||||
|  | ||||
| class UserAbsenceSyncException extends \LogicException | ||||
| { | ||||
|     public function __construct(string $message = '', int $code = 20_230_706, \Throwable $previous = null) | ||||
|     public function __construct(string $message = '', int $code = 20_230_706, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct($message, $code, $previous); | ||||
|     } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -26,7 +27,8 @@ final readonly class JobFilter implements FilterInterface | ||||
|     private const PREFIX = 'cal_filter_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private TranslatableStringHelper $translatableStringHelper | ||||
|         private TranslatableStringHelper $translatableStringHelper, | ||||
|         private UserJobRepositoryInterface $userJobRepository | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -74,6 +76,7 @@ final readonly class JobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('job', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( | ||||
|                     $j->getLabel() | ||||
|                 ), | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User\UserScopeHistory; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -28,7 +29,8 @@ class ScopeFilter implements FilterInterface | ||||
|  | ||||
|     public function __construct( | ||||
|         protected TranslatorInterface $translator, | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper, | ||||
|         private readonly ScopeRepositoryInterface $scopeRepository | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -76,6 +78,7 @@ class ScopeFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('scope', EntityType::class, [ | ||||
|                 'class' => Scope::class, | ||||
|                 'choices' => $this->scopeRepository->findAllActive(), | ||||
|                 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getName() | ||||
|                 ), | ||||
|   | ||||
| @@ -33,7 +33,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface | ||||
|     /** | ||||
|      * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft | ||||
|      */ | ||||
|     public function isUserAbsent(User $user): null|bool | ||||
|     public function isUserAbsent(User $user): ?bool | ||||
|     { | ||||
|         $id = $this->mapCalendarToUser->getUserId($user); | ||||
|  | ||||
|   | ||||
| @@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface | ||||
|     /** | ||||
|      * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft | ||||
|      */ | ||||
|     public function isUserAbsent(User $user): null|bool; | ||||
|     public function isUserAbsent(User $user): ?bool; | ||||
| } | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class MachineHttpClient implements HttpClientInterface | ||||
|  | ||||
|     private readonly HttpClientInterface $decoratedClient; | ||||
|  | ||||
|     public function __construct(private readonly MachineTokenStorage $machineTokenStorage, HttpClientInterface $decoratedClient = null) | ||||
|     public function __construct(private readonly MachineTokenStorage $machineTokenStorage, ?HttpClientInterface $decoratedClient = null) | ||||
|     { | ||||
|         $this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create(); | ||||
|     } | ||||
| @@ -68,7 +68,7 @@ class MachineHttpClient implements HttpClientInterface | ||||
|         return $this->decoratedClient->request($method, $url, $options); | ||||
|     } | ||||
|  | ||||
|     public function stream($responses, float $timeout = null): ResponseStreamInterface | ||||
|     public function stream($responses, ?float $timeout = null): ResponseStreamInterface | ||||
|     { | ||||
|         return $this->decoratedClient->stream($responses, $timeout); | ||||
|     } | ||||
|   | ||||
| @@ -178,8 +178,8 @@ class MapCalendarToUser | ||||
|     public function writeSubscriptionMetadata( | ||||
|         User $user, | ||||
|         int $expiration, | ||||
|         string $id = null, | ||||
|         string $secret = null | ||||
|         ?string $id = null, | ||||
|         ?string $secret = null | ||||
|     ): void { | ||||
|         $user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration); | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class OnBehalfOfUserHttpClient | ||||
|  | ||||
|     private readonly HttpClientInterface $decoratedClient; | ||||
|  | ||||
|     public function __construct(private readonly OnBehalfOfUserTokenStorage $tokenStorage, HttpClientInterface $decoratedClient = null) | ||||
|     public function __construct(private readonly OnBehalfOfUserTokenStorage $tokenStorage, ?HttpClientInterface $decoratedClient = null) | ||||
|     { | ||||
|         $this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create(); | ||||
|     } | ||||
| @@ -63,7 +63,7 @@ class OnBehalfOfUserHttpClient | ||||
|         return $this->decoratedClient->request($method, $url, $options); | ||||
|     } | ||||
|  | ||||
|     public function stream($responses, float $timeout = null): ResponseStreamInterface | ||||
|     public function stream($responses, ?float $timeout = null): ResponseStreamInterface | ||||
|     { | ||||
|         return $this->decoratedClient->stream($responses, $timeout); | ||||
|     } | ||||
|   | ||||
| @@ -171,7 +171,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void | ||||
|     { | ||||
|         if ('' === $remoteId) { | ||||
|             return; | ||||
|   | ||||
| @@ -46,7 +46,7 @@ class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void | ||||
|     { | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -47,7 +47,7 @@ interface RemoteCalendarConnectorInterface | ||||
|      */ | ||||
|     public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array; | ||||
|  | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void; | ||||
|     public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void; | ||||
|  | ||||
|     public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void; | ||||
|  | ||||
|   | ||||
| @@ -159,7 +159,7 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface | ||||
|     /** | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array | ||||
|     { | ||||
|         $qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('c'); | ||||
|  | ||||
| @@ -178,7 +178,7 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array | ||||
|     public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array | ||||
|     { | ||||
|         $qb = $this->buildQueryByPerson($person, $startDate, $endDate) | ||||
|             ->select('c'); | ||||
|   | ||||
| @@ -46,7 +46,7 @@ interface CalendarACLAwareRepositoryInterface | ||||
|     /** | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array; | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array; | ||||
|  | ||||
|     /** | ||||
|      * Return all the calendars which are associated with a person, either on @see{Calendar::person} or within. | ||||
| @@ -58,5 +58,5 @@ interface CalendarACLAwareRepositoryInterface | ||||
|      * | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array; | ||||
|     public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array; | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn | ||||
|         return $this->repository->findAll(); | ||||
|     } | ||||
|  | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null) | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ interface CalendarDocRepositoryInterface | ||||
|     /** | ||||
|      * @return array|CalendarDoc[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null); | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null); | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?CalendarDoc; | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class CalendarRangeRepository implements ObjectRepository | ||||
|     /** | ||||
|      * @return array|CalendarRange[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null) | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
| @@ -64,8 +64,8 @@ class CalendarRangeRepository implements ObjectRepository | ||||
|         User $user, | ||||
|         \DateTimeImmutable $from, | ||||
|         \DateTimeImmutable $to, | ||||
|         int $limit = null, | ||||
|         int $offset = null | ||||
|         ?int $limit = null, | ||||
|         ?int $offset = null | ||||
|     ): array { | ||||
|         $qb = $this->buildQueryAvailableRangesForUser($user, $from, $to); | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ class CalendarRepository implements ObjectRepository | ||||
|             ->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     public function createQueryBuilder(string $alias, string $indexBy = null): QueryBuilder | ||||
|     public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder | ||||
|     { | ||||
|         return $this->repository->createQueryBuilder($alias, $indexBy); | ||||
|     } | ||||
| @@ -67,7 +67,7 @@ class CalendarRepository implements ObjectRepository | ||||
|     /** | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
| @@ -75,7 +75,7 @@ class CalendarRepository implements ObjectRepository | ||||
|     /** | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, array $orderBy = null, int $limit = null, int $offset = null): array | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->findBy( | ||||
|             [ | ||||
| @@ -87,7 +87,7 @@ class CalendarRepository implements ObjectRepository | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function findByNotificationAvailable(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate, int $limit = null, int $offset = null): array | ||||
|     public function findByNotificationAvailable(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         $qb = $this->queryByNotificationAvailable($startDate, $endDate)->select('c'); | ||||
|  | ||||
| @@ -105,7 +105,7 @@ class CalendarRepository implements ObjectRepository | ||||
|     /** | ||||
|      * @return array|Calendar[] | ||||
|      */ | ||||
|     public function findByUser(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to, int $limit = null, int $offset = null): array | ||||
|     public function findByUser(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         $qb = $this->buildQueryByUser($user, $from, $to)->select('c'); | ||||
|  | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class InviteRepository implements ObjectRepository | ||||
|     /** | ||||
|      * @return array|Invite[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null) | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) | ||||
|     { | ||||
|         return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|   | ||||
| @@ -44,7 +44,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen | ||||
|     /** | ||||
|      * @throws MappingException | ||||
|      */ | ||||
|     public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         $classMetadata = $this->em->getClassMetadata(CalendarDoc::class); | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
| @@ -91,7 +91,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen | ||||
|         return $this->security->isGranted(CalendarVoter::SEE, $accompanyingPeriod); | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         $classMetadata = $this->em->getClassMetadata(CalendarDoc::class); | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|   | ||||
| @@ -40,7 +40,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     private function addWhereClausesToQuery(FetchQuery $query, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery | ||||
|     private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|  | ||||
| @@ -74,7 +74,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe | ||||
|     /** | ||||
|      * @throws MappingException | ||||
|      */ | ||||
|     public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface | ||||
|     public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         $classMetadata = $this->em->getClassMetadata(CalendarDoc::class); | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|   | ||||
| @@ -202,8 +202,8 @@ final class CalendarContextTest extends TestCase | ||||
|     } | ||||
|  | ||||
|     private function buildCalendarContext( | ||||
|         EntityManagerInterface $entityManager = null, | ||||
|         NormalizerInterface $normalizer = null | ||||
|         ?EntityManagerInterface $entityManager = null, | ||||
|         ?NormalizerInterface $normalizer = null | ||||
|     ): CalendarContext { | ||||
|         $baseContext = $this->prophesize(BaseContextData::class); | ||||
|         $baseContext->getData(null)->willReturn(['base_context' => 'data']); | ||||
|   | ||||
| @@ -363,7 +363,7 @@ class CustomFieldsGroupController extends AbstractController | ||||
|      * | ||||
|      * @return \Symfony\Component\Form\Form | ||||
|      */ | ||||
|     private function createMakeDefaultForm(CustomFieldsGroup $group = null) | ||||
|     private function createMakeDefaultForm(?CustomFieldsGroup $group = null) | ||||
|     { | ||||
|         return $this->createFormBuilder($group, [ | ||||
|             'method' => 'POST', | ||||
|   | ||||
| @@ -156,7 +156,7 @@ class CustomFieldChoice extends AbstractCustomField | ||||
|         return $serialized; | ||||
|     } | ||||
|  | ||||
|     public function extractOtherValue(CustomField $cf, array $data = null) | ||||
|     public function extractOtherValue(CustomField $cf, ?array $data = null) | ||||
|     { | ||||
|         return $data['_other']; | ||||
|     } | ||||
| @@ -355,7 +355,7 @@ class CustomFieldChoice extends AbstractCustomField | ||||
|      * If the value had an 'allow_other' = true option, the returned value | ||||
|      * **is not** the content of the _other field, but the `_other` string. | ||||
|      */ | ||||
|     private function guessValue(null|array|string $value) | ||||
|     private function guessValue(array|string|null $value) | ||||
|     { | ||||
|         if (null === $value) { | ||||
|             return null; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ class CustomField | ||||
|      *     targetEntity="Chill\CustomFieldsBundle\Entity\CustomFieldsGroup", | ||||
|      * inversedBy="customFields") | ||||
|      */ | ||||
|     private ?\Chill\CustomFieldsBundle\Entity\CustomFieldsGroup $customFieldGroup = null; | ||||
|     private ?CustomFieldsGroup $customFieldGroup = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Id | ||||
| @@ -212,7 +212,7 @@ class CustomField | ||||
|      * | ||||
|      * @return CustomField | ||||
|      */ | ||||
|     public function setCustomFieldsGroup(CustomFieldsGroup $customFieldGroup = null) | ||||
|     public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldGroup = null) | ||||
|     { | ||||
|         $this->customFieldGroup = $customFieldGroup; | ||||
|  | ||||
|   | ||||
| @@ -63,7 +63,7 @@ class Option | ||||
|      * | ||||
|      * @ORM\JoinColumn(nullable=true) | ||||
|      */ | ||||
|     private ?\Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option $parent = null; | ||||
|     private ?Option $parent = null; | ||||
|  | ||||
|     /** | ||||
|      * A json representation of text (multilingual). | ||||
| @@ -182,7 +182,7 @@ class Option | ||||
|     /** | ||||
|      * @return $this | ||||
|      */ | ||||
|     public function setParent(Option $parent = null) | ||||
|     public function setParent(?Option $parent = null) | ||||
|     { | ||||
|         $this->parent = $parent; | ||||
|         $this->key = $parent->getKey(); | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class CustomFieldsDefaultGroup | ||||
|      * | ||||
|      *     sf4 check: option inversedBy="customFields" return inconsistent error mapping !! | ||||
|      */ | ||||
|     private ?\Chill\CustomFieldsBundle\Entity\CustomFieldsGroup $customFieldsGroup = null; | ||||
|     private ?CustomFieldsGroup $customFieldsGroup = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="string", length=255) | ||||
|   | ||||
| @@ -139,7 +139,7 @@ class CustomFieldsGroup | ||||
|     /** | ||||
|      * Get name. | ||||
|      */ | ||||
|     public function getName(string $language = null): array|string | ||||
|     public function getName(?string $language = null): array|string | ||||
|     { | ||||
|         // TODO set this in a service, PLUS twig function | ||||
|         if (null !== $language) { | ||||
|   | ||||
| @@ -84,7 +84,7 @@ class CustomFieldProvider implements ContainerAwareInterface | ||||
|      * | ||||
|      * @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer() | ||||
|      */ | ||||
|     public function setContainer(ContainerInterface $container = null) | ||||
|     public function setContainer(?ContainerInterface $container = null) | ||||
|     { | ||||
|         if (null === $container) { | ||||
|             throw new \LogicException('container should not be null'); | ||||
|   | ||||
| @@ -25,7 +25,7 @@ final class CustomFieldsHelperTest extends KernelTestCase | ||||
| { | ||||
|     private ?object $cfHelper = null; | ||||
|  | ||||
|     private \Chill\CustomFieldsBundle\Entity\CustomField $randomCFText; | ||||
|     private CustomField $randomCFText; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|   | ||||
| @@ -16,29 +16,42 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; | ||||
| use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FileType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\EmailType; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| // TODO à mettre dans services | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||||
| use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||
| use Symfony\Component\Messenger\MessageBusInterface; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| use Symfony\Component\Validator\Constraints\NotNull; | ||||
|  | ||||
| final class DocGeneratorTemplateController extends AbstractController | ||||
| { | ||||
|     public function __construct(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager) | ||||
|     { | ||||
|     public function __construct( | ||||
|         private readonly ContextManager $contextManager, | ||||
|         private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         private readonly MessageBusInterface $messageBus, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly ClockInterface $clock, | ||||
|         private readonly Security $security, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -163,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|             throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)); | ||||
|         } | ||||
|  | ||||
|         $contextGenerationData = [ | ||||
|             'test_file' => null, | ||||
|         ]; | ||||
|         $contextGenerationData = []; | ||||
|  | ||||
|         if ( | ||||
|             $context instanceof DocGeneratorContextWithPublicFormInterface | ||||
| @@ -175,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|                 $builder = $this->createFormBuilder( | ||||
|                     array_merge( | ||||
|                         $context->getFormData($template, $entity), | ||||
|                         $isTest ? ['test_file' => null, 'show_data' => false] : [] | ||||
|                         $isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : [] | ||||
|                     ) | ||||
|                 ); | ||||
|  | ||||
|                 $context->buildPublicForm($builder, $template, $entity); | ||||
|             } else { | ||||
|                 $builder = $this->createFormBuilder( | ||||
|                     ['test_file' => null, 'show_data' => false] | ||||
|                     ['creator' => null, 'show_data' => false, 'send_result_to' => ''] | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             if ($isTest) { | ||||
|                 $builder->add('test_file', FileType::class, [ | ||||
|                     'label' => 'Template file', | ||||
|                 $builder->add('dump_only', CheckboxType::class, [ | ||||
|                     'label' => 'docgen.Show data instead of generating', | ||||
|                     'required' => false, | ||||
|                 ]); | ||||
|                 $builder->add('show_data', CheckboxType::class, [ | ||||
|                     'label' => 'Show data instead of generating', | ||||
|                     'required' => false, | ||||
|                 $builder->add('send_result_to', EmailType::class, [ | ||||
|                     'label' => 'docgen.Send report to', | ||||
|                     'help' => 'docgen.Send report errors to this email address', | ||||
|                     'empty_data' => '', | ||||
|                     'required' => true, | ||||
|                     'constraints' => [ | ||||
|                         new NotBlank(), | ||||
|                         new NotNull(), | ||||
|                     ], | ||||
|                 ]); | ||||
|                 $builder->add('creator', PickUserDynamicType::class, [ | ||||
|                     'label' => 'docgen.Generate as creator', | ||||
|                     'help' => 'docgen.The document will be generated as the given creator', | ||||
|                     'multiple' => false, | ||||
|                     'constraints' => [ | ||||
|                         new NotNull(), | ||||
|                     ], | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
| @@ -204,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|             } elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) { | ||||
|                 $templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig'; | ||||
|                 $templateOptions = [ | ||||
|                     'entity' => $entity, 'form' => $form->createView(), | ||||
|                     'template' => $template, 'context' => $context, | ||||
|                     'entity' => $entity, | ||||
|                     'form' => $form->createView(), | ||||
|                     'template' => $template, | ||||
|                     'context' => $context, | ||||
|                 ]; | ||||
|  | ||||
|                 return $this->render($templatePath, $templateOptions); | ||||
| @@ -218,43 +245,21 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|                 $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) | ||||
|                 : []; | ||||
|  | ||||
|         // if is test, render the data or generate the doc | ||||
|         if ($isTest && isset($form) && $form['show_data']->getData()) { | ||||
|             return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ | ||||
|                 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), \JSON_PRETTY_PRINT), | ||||
|             ]); | ||||
|         } | ||||
|         if ($isTest) { | ||||
|             $generated = $this->generator->generateDocFromTemplate( | ||||
|                 $template, | ||||
|                 $entityId, | ||||
|                 $contextGenerationDataSanitized, | ||||
|                 null, | ||||
|                 true, | ||||
|                 isset($form) ? $form['test_file']->getData() : null | ||||
|             ); | ||||
|  | ||||
|             return new Response( | ||||
|                 $generated, | ||||
|                 Response::HTTP_OK, | ||||
|                 [ | ||||
|                     'Content-Transfer-Encoding', 'binary', | ||||
|                     'Content-Type' => 'application/vnd.oasis.opendocument.text', | ||||
|                     'Content-Disposition' => 'attachment; filename="generated.odt"', | ||||
|                     'Content-Length' => \strlen($generated), | ||||
|                 ], | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // this is not a test | ||||
|         // we prepare the object to store the document | ||||
|         $storedObject = (new StoredObject()) | ||||
|             ->setStatus(StoredObject::STATUS_PENDING) | ||||
|         ; | ||||
|  | ||||
|         if ($isTest) { | ||||
|             // document will be stored during 15 days, if generation is a test | ||||
|             $storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D'))); | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->persist($storedObject); | ||||
|  | ||||
|         // we store the generated document | ||||
|         // we store the generated document (associate with the original entity, etc.) | ||||
|         // but only if this is not a test | ||||
|         if (!$isTest) { | ||||
|             $context | ||||
|                 ->storeGenerated( | ||||
|                     $template, | ||||
| @@ -262,16 +267,35 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|                     $entity, | ||||
|                     $contextGenerationData | ||||
|                 ); | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|  | ||||
|         if ($isTest) { | ||||
|             $creator = $contextGenerationData['creator']; | ||||
|             $sendResultTo =  ($form ?? null)?->get('send_result_to')?->getData() ?? null; | ||||
|             $dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false; | ||||
|         } else { | ||||
|             $creator = $this->security->getUser(); | ||||
|  | ||||
|             if (!$creator instanceof User) { | ||||
|                 throw new AccessDeniedHttpException('only authenticated user can request a generation'); | ||||
|             } | ||||
|  | ||||
|             $sendResultTo = null; | ||||
|             $dumpOnly = false; | ||||
|         } | ||||
|  | ||||
|         $this->messageBus->dispatch( | ||||
|             new RequestGenerationMessage( | ||||
|                 $this->getUser(), | ||||
|                 $creator, | ||||
|                 $template, | ||||
|                 $entityId, | ||||
|                 $storedObject, | ||||
|                 $contextGenerationDataSanitized, | ||||
|                 $isTest, | ||||
|                 $sendResultTo, | ||||
|                 $dumpOnly, | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|   | ||||
| @@ -69,7 +69,7 @@ class DocGeneratorTemplate | ||||
|      * | ||||
|      * @Serializer\Groups({"read"}) | ||||
|      */ | ||||
|     private int $id; | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="json") | ||||
|   | ||||
| @@ -13,5 +13,5 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver; | ||||
|  | ||||
| interface DriverInterface | ||||
| { | ||||
|     public function generateFromString(string $template, string $resourceType, array $data, string $templateName = null): string; | ||||
|     public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string; | ||||
| } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver\Exception; | ||||
|  */ | ||||
| class TemplateException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(private readonly array $errors, $code = 0, \Throwable $previous = null) | ||||
|     public function __construct(private readonly array $errors, $code = 0, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct('Error while generating document from template', $code, $previous); | ||||
|     } | ||||
|   | ||||
| @@ -33,7 +33,7 @@ final class RelatorioDriver implements DriverInterface | ||||
|         $this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url']; | ||||
|     } | ||||
|  | ||||
|     public function generateFromString(string $template, string $resourceType, array $data, string $templateName = null): string | ||||
|     public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string | ||||
|     { | ||||
|         $form = new FormDataPart( | ||||
|             [ | ||||
|   | ||||
| @@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
|  | ||||
| final class DocGeneratorTemplateRepository implements ObjectRepository | ||||
| final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface | ||||
| { | ||||
|     private readonly EntityRepository $repository; | ||||
|  | ||||
| @@ -58,7 +57,7 @@ final class DocGeneratorTemplateRepository implements ObjectRepository | ||||
|      * | ||||
|      * @return DocGeneratorTemplate[] | ||||
|      */ | ||||
|     public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
| @@ -85,7 +84,7 @@ final class DocGeneratorTemplateRepository implements ObjectRepository | ||||
|             ->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function findOneBy(array $criteria, array $orderBy = null): ?DocGeneratorTemplate | ||||
|     public function findOneBy(array $criteria, ?array $orderBy = null): ?DocGeneratorTemplate | ||||
|     { | ||||
|         return $this->repository->findOneBy($criteria, $orderBy); | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Repository; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @extends ObjectRepository<DocGeneratorTemplate> | ||||
|  */ | ||||
| interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository | ||||
| { | ||||
|     public function countByEntity(string $entity): int; | ||||
| } | ||||
| @@ -1,36 +1,62 @@ | ||||
| {% extends '@ChillMain/CRUD/Admin/index.html.twig' %} | ||||
|  | ||||
| {% block js %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_script_tags('mod_document_action_buttons_group') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block css %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_link_tags('mod_document_action_buttons_group') }} | ||||
| {% endblock %} | ||||
|  | ||||
|  | ||||
| {% block admin_content %} | ||||
|     {% embed '@ChillMain/CRUD/_index.html.twig' %} | ||||
|         {% block table_entities_thead_tr %} | ||||
|             <th></th> | ||||
|             <th>{{ 'Title'|trans }}</th> | ||||
|             <th>{{ 'docgen.Context'|trans }}</th> | ||||
|             <th>{{ 'docgen.test generate'|trans }}</th> | ||||
|             <th>{{ 'Edit'|trans }}</th> | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block table_entities_tbody %} | ||||
|             {% if entities|length == 0 %} | ||||
|                 <p class="chill-no-data-statement">{{ 'docgen.Any template configured'|trans }}</p> | ||||
|             {% else %} | ||||
|                 <div class="flex-table"> | ||||
|                     {% for entity in entities %} | ||||
|                 <tr> | ||||
|                     <td>{{ entity.id }}</td> | ||||
|                     <td>{{ entity.name|localize_translatable_string}}</td> | ||||
|                     <td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td> | ||||
|                     <td> | ||||
|                             <div class="item-bloc"> | ||||
|                                 <div class="item-row"> | ||||
|                                     <div class="item-col" style="flex-basis:100%;"> | ||||
|                                         <h2>{{ entity.name|localize_translatable_string }}</h2> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                                 <div class="item-row"> | ||||
|                                     <p><span class="badge bg-chill-green-dark">{{ contextManager.getContextByKey(entity.context).name|trans }}</span></p> | ||||
|                                 </div> | ||||
|                                 <div class="item-row"> | ||||
|                                     <div class="item-col"></div> | ||||
|                                     <ul class="record_actions item-col flex-shrink-1"> | ||||
|                                         <li> | ||||
|                                             <form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}"> | ||||
|                             <input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', '/')|e('html_attr') }}" /> | ||||
|                                                 <input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', app.request.uri)|e('html_attr') }}" /> | ||||
|                                                 <input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" /> | ||||
|                                                 <input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" /> | ||||
|                             <input type="text" name="entityId" /> | ||||
|                                                 <input type="text" name="entityId" placeholder="{{ 'docgen.entity_id_placeholder'|trans }}" required /> | ||||
|  | ||||
|                                                 <button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button> | ||||
|                                             </form> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             {{ entity.file|chill_document_button_group('Template file', true) }} | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             <a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                                         </li> | ||||
|                                     </ul> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                     {% endfor %} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|  | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block actions_before %} | ||||
|   | ||||
| @@ -6,20 +6,22 @@ | ||||
| <div class="col-md-10 col-xxl"> | ||||
|  | ||||
|     <h1>{{ block('title') }}</h1> | ||||
|    <div class="container"> | ||||
|    <div class="container overflow-hidden"> | ||||
|        {% for key, context in contexts %} | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-4"> | ||||
|             <div class="row g-3" style="margin-top: 1rem;"> | ||||
|                 <div class="col-4 offset-1 text-center"> | ||||
|                     <a | ||||
|                         href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}" | ||||
|                         class="btn btn-outline-chill-green-dark"> | ||||
|                         {{ context.name|trans }} | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div class="col-md-8"> | ||||
|                 <div class="col"> | ||||
|                     <div> | ||||
|                         {{ context.description|trans|nl2br }} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|        {% endfor %} | ||||
|    </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {{ creator.label }}, | ||||
| {% if creator is not same as null %}{{ creator.label }},{% endif %} | ||||
|  | ||||
| {{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} | ||||
| {{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }} | ||||
|  | ||||
| {{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| {{ 'docgen.data_dump_email.Dear'|trans }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }} | ||||
|  | ||||
| {{ link }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }} | ||||
| @@ -20,7 +20,7 @@ class NormalizeNullValueHelper | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ClassMetadataInterface $classMetadata = null) | ||||
|     public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ?ClassMetadataInterface $classMetadata = null) | ||||
|     { | ||||
|         $data = []; | ||||
|         $data['isNull'] = true; | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class BaseContextData | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public function getData(User $user = null): array | ||||
|     public function getData(?User $user = null): array | ||||
|     { | ||||
|         $data = []; | ||||
|  | ||||
|   | ||||
| @@ -17,54 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; | ||||
| use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\HttpFoundation\File\File; | ||||
| use Symfony\Component\Yaml\Yaml; | ||||
|  | ||||
| class Generator implements GeneratorInterface | ||||
| { | ||||
|     private const LOG_PREFIX = '[docgen generator] '; | ||||
|  | ||||
|     public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager) | ||||
|     { | ||||
|     public function __construct( | ||||
|         private readonly ContextManagerInterface $contextManager, | ||||
|         private readonly DriverInterface $driver, | ||||
|         private readonly ManagerRegistry $objectManagerRegistry, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly StoredObjectManagerInterface $storedObjectManager | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function generateDataDump( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject { | ||||
|         return $this->generateFromTemplate( | ||||
|             $template, | ||||
|             $entityId, | ||||
|             $contextGenerationDataNormalized, | ||||
|             $destinationStoredObject, | ||||
|             $creator, | ||||
|             $clearEntityManagerDuringProcess, | ||||
|             true, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @template T of File|null | ||||
|      * @template B of bool | ||||
|      * | ||||
|      * @param B                      $isTest | ||||
|      * @param (B is true ? T : null) $testFile | ||||
|      * | ||||
|      * @psalm-return (B is true ? string : null) | ||||
|      * | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable | ||||
|      */ | ||||
|     public function generateDocFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject = null, | ||||
|         bool $isTest = false, | ||||
|         File $testFile = null, | ||||
|         User $creator = null | ||||
|     ): ?string { | ||||
|         if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject { | ||||
|         return $this->generateFromTemplate( | ||||
|             $template, | ||||
|             $entityId, | ||||
|             $contextGenerationDataNormalized, | ||||
|             $destinationStoredObject, | ||||
|             $creator, | ||||
|             $clearEntityManagerDuringProcess, | ||||
|             false, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function generateFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|         bool $generateDumpOnly = false, | ||||
|     ): StoredObject { | ||||
|         if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); | ||||
|             throw new ObjectReadyException(); | ||||
|         } | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ | ||||
|             'entity_id' => $entityId, | ||||
|             'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), | ||||
|             'destination_stored_object' => $destinationStoredObject->getId(), | ||||
|         ]); | ||||
|  | ||||
|         $context = $this->contextManager->getContextByDocGeneratorTemplate($template); | ||||
|  | ||||
|         $entity = $this | ||||
|             ->entityManager | ||||
|             ->objectManagerRegistry | ||||
|             ->getManagerForClass($context->getEntityClass()) | ||||
|             ->find($context->getEntityClass(), $entityId) | ||||
|         ; | ||||
|  | ||||
| @@ -82,17 +116,47 @@ class Generator implements GeneratorInterface | ||||
|  | ||||
|         $data = $context->getData($template, $entity, $contextGenerationDataNormalized); | ||||
|  | ||||
|         $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; | ||||
|         $this->entityManager->clear(); | ||||
|         $destinationStoredObjectId = $destinationStoredObject->getId(); | ||||
|  | ||||
|         if ($clearEntityManagerDuringProcess) { | ||||
|             // we clean the entity manager | ||||
|             $this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear(); | ||||
|  | ||||
|             // this will force php to clean the memory | ||||
|             gc_collect_cycles(); | ||||
|         if (null !== $destinationStoredObjectId) { | ||||
|             $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); | ||||
|         } | ||||
|  | ||||
|         if ($isTest && ($testFile instanceof File)) { | ||||
|             $templateDecrypted = file_get_contents($testFile->getPathname()); | ||||
|         } else { | ||||
|         // as we potentially deleted the storedObject from memory, we have to restore it | ||||
|         $destinationStoredObject = $this->objectManagerRegistry | ||||
|             ->getManagerForClass(StoredObject::class) | ||||
|             ->find(StoredObject::class, $destinationStoredObjectId); | ||||
|  | ||||
|         if ($generateDumpOnly) { | ||||
|             $content = Yaml::dump($data, 6); | ||||
|             /* @var StoredObject $destinationStoredObject */ | ||||
|             $destinationStoredObject | ||||
|                 ->setType('application/yaml') | ||||
|                 ->setFilename(sprintf('%s_yaml', uniqid('doc_', true))) | ||||
|                 ->setStatus(StoredObject::STATUS_READY) | ||||
|             ; | ||||
|  | ||||
|             try { | ||||
|                 $this->storedObjectManager->write($destinationStoredObject, $content); | ||||
|             } catch (StoredObjectManagerException $e) { | ||||
|                 $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|  | ||||
|                 throw new GeneratorException([$e->getMessage()], $e); | ||||
|             } | ||||
|  | ||||
|             return $destinationStoredObject; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             $templateDecrypted = $this->storedObjectManager->read($template->getFile()); | ||||
|         } catch (StoredObjectManagerException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|  | ||||
|             throw new GeneratorException([$e->getMessage()], $e); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
| @@ -105,19 +169,10 @@ class Generator implements GeneratorInterface | ||||
|                     $template->getFile()->getFilename() | ||||
|                 ); | ||||
|         } catch (TemplateException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors())); | ||||
|             throw new GeneratorException($e->getErrors(), $e); | ||||
|         } | ||||
|  | ||||
|         if (true === $isTest) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ | ||||
|                 'is_test' => true, | ||||
|                 'entity_id' => $entityId, | ||||
|                 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), | ||||
|             ]); | ||||
|  | ||||
|             return $generatedResource; | ||||
|         } | ||||
|  | ||||
|         /* @var StoredObject $destinationStoredObject */ | ||||
|         $destinationStoredObject | ||||
|             ->setType($template->getFile()->getType()) | ||||
| @@ -125,15 +180,19 @@ class Generator implements GeneratorInterface | ||||
|             ->setStatus(StoredObject::STATUS_READY) | ||||
|         ; | ||||
|  | ||||
|         try { | ||||
|             $this->storedObjectManager->write($destinationStoredObject, $generatedResource); | ||||
|         } catch (StoredObjectManagerException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|             throw new GeneratorException([$e->getMessage()], $e); | ||||
|         } | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ | ||||
|             'entity_id' => $entityId, | ||||
|             'destination_stored_object' => $destinationStoredObject->getId(), | ||||
|         ]); | ||||
|  | ||||
|         return null; | ||||
|         return $destinationStoredObject; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class GeneratorException extends \RuntimeException | ||||
|     /** | ||||
|      * @param string[] $errors | ||||
|      */ | ||||
|     public function __construct(private readonly array $errors = [], \Throwable $previous = null) | ||||
|     public function __construct(private readonly array $errors = [], ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct( | ||||
|             'Could not generate the document', | ||||
|   | ||||
| @@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Symfony\Component\HttpFoundation\File\File; | ||||
|  | ||||
| interface GeneratorInterface | ||||
| { | ||||
|     /** | ||||
|      * @template T of File|null | ||||
|      * @template B of bool | ||||
|      * Generate a document and store the document on disk. | ||||
|      * | ||||
|      * @param B                      $isTest | ||||
|      * @param (B is true ? T : null) $testFile | ||||
|      * The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored | ||||
|      * into the object. The number of generation trial will also be incremented. | ||||
|      * | ||||
|      * @psalm-return (B is true ? string : null) | ||||
|      * This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process, | ||||
|      * unless the paarameter `$clearEntityManagerDuringProcess` is set on false. | ||||
|      * | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable | ||||
|      * As the entity manager might be cleaned, the new instance of the stored object will be returned by this method. | ||||
|      * | ||||
|      * Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`). | ||||
|      * | ||||
|      * @phpstan-impure | ||||
|      * | ||||
|      * @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials | ||||
|      * | ||||
|      * @throws StoredObjectManagerException if unable to decrypt the template or store the document | ||||
|      */ | ||||
|     public function generateDocFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject = null, | ||||
|         bool $isTest = false, | ||||
|         File $testFile = null, | ||||
|         User $creator = null | ||||
|     ): ?string; | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject; | ||||
|  | ||||
|     /** | ||||
|      * Generate a data dump, and store it within the `$destinationStoredObject`. | ||||
|      */ | ||||
|     public function generateDataDump( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject; | ||||
| } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ namespace Chill\DocGeneratorBundle\Service\Generator; | ||||
|  | ||||
| class RelatedEntityNotFoundException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(string $relatedEntityClass, int $relatedEntityId, \Throwable $previous = null) | ||||
|     public function __construct(string $relatedEntityClass, int $relatedEntityId, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct( | ||||
|             sprintf('Related entity not found: %s, %s', $relatedEntityClass, $relatedEntityId), | ||||
|   | ||||
| @@ -0,0 +1,64 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; | ||||
|  | ||||
| /** | ||||
|  * The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache | ||||
|  * after a specific message is handled or fails. | ||||
|  */ | ||||
| final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private StoredObjectManagerInterface $storedObjectManager, | ||||
|         private LoggerInterface $logger, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public static function getSubscribedEvents() | ||||
|     { | ||||
|         return [ | ||||
|             WorkerMessageHandledEvent::class => [ | ||||
|                 ['afterHandling', 0], | ||||
|             ], | ||||
|             WorkerMessageFailedEvent::class => [ | ||||
|                 ['afterFails', 0], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function afterHandling(WorkerMessageHandledEvent $event): void | ||||
|     { | ||||
|         if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|             $this->clearStoredObjectCache(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function afterFails(WorkerMessageFailedEvent $event): void | ||||
|     { | ||||
|         if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|             $this->clearStoredObjectCache(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private function clearStoredObjectCache(): void | ||||
|     { | ||||
|         $this->logger->debug('clear the cache after generation of a document'); | ||||
|  | ||||
|         $this->storedObjectManager->clearCache(); | ||||
|     } | ||||
| } | ||||
| @@ -11,10 +11,11 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| @@ -24,12 +25,22 @@ use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * @see OnGenerationFailsTest for test suite | ||||
|  */ | ||||
| final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
| { | ||||
|     public const LOG_PREFIX = '[docgen failed] '; | ||||
|  | ||||
|     public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository) | ||||
|     { | ||||
|     public function __construct( | ||||
|         private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository, | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private LoggerInterface $logger, | ||||
|         private MailerInterface $mailer, | ||||
|         private StoredObjectRepositoryInterface $storedObjectRepository, | ||||
|         private TranslatorInterface $translator, | ||||
|         private UserRepositoryInterface $userRepository | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public static function getSubscribedEvents() | ||||
| @@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|         $message = $event->getEnvelope()->getMessage(); | ||||
|  | ||||
|         if (!$message instanceof RequestGenerationMessage) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         /** @var RequestGenerationMessage $message */ | ||||
|         $message = $event->getEnvelope()->getMessage(); | ||||
|  | ||||
|         $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ | ||||
|             'stored_object_id' => $message->getDestinationStoredObjectId(), | ||||
|             'entity_id' => $message->getEntityId(), | ||||
| @@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|  | ||||
|     private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void | ||||
|     { | ||||
|         $creatorId = $message->getCreatorId(); | ||||
|  | ||||
|         if (null === $creator = $this->userRepository->find($creatorId)) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (null === $creator->getEmail() || '' === $creator->getEmail()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); | ||||
|         if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'No email associated with this request generation'); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
| @@ -96,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|         // if the exception is not a GeneratorException, we try the previous one... | ||||
|         $throwable = $event->getThrowable(); | ||||
|         if (!$throwable instanceof GeneratorException) { | ||||
|             $throwable = $throwable->getPrevious(); | ||||
|             $throwable = $throwable->getPrevious() ?? $throwable; | ||||
|         } | ||||
|  | ||||
|         if ($throwable instanceof GeneratorException) { | ||||
| @@ -111,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (null === $creator = $this->userRepository->find($message->getCreatorId())) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Creator not found'); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $email = (new TemplatedEmail()) | ||||
|             ->to($creator->getEmail()) | ||||
|             ->to($message->getSendResultToEmail()) | ||||
|             ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) | ||||
|             ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') | ||||
|             ->context([ | ||||
|   | ||||
| @@ -11,15 +11,21 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\Generator; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Bridge\Twig\Mime\TemplatedEmail; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; | ||||
| use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * Handle the request of document generation. | ||||
| @@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|  | ||||
|     private const LOG_PREFIX = '[docgen message handler] '; | ||||
|  | ||||
|     public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository) | ||||
|     { | ||||
|     public function __construct( | ||||
|         private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly Generator $generator, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly StoredObjectRepository $storedObjectRepository, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|         private readonly MailerInterface $mailer, | ||||
|         private readonly TempUrlGeneratorInterface $tempUrlGenerator, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function __invoke(RequestGenerationMessage $message) | ||||
| @@ -45,30 +60,83 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|         } | ||||
|  | ||||
|         if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [ | ||||
|                 'template_id' => $message->getTemplateId(), | ||||
|                 'destination_stored_object' => $message->getDestinationStoredObjectId(), | ||||
|                 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), | ||||
|             ]); | ||||
|  | ||||
|             throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); | ||||
|         } | ||||
|  | ||||
|         $creator = $this->userRepository->find($message->getCreatorId()); | ||||
|  | ||||
|         // we increase the number of generation trial in the object, and, in the same time, update the counter | ||||
|         // on the database side. This ensure that, if the script fails for any reason (memory limit reached), the | ||||
|         // counter is inscreased | ||||
|         $destinationStoredObject->addGenerationTrial(); | ||||
|         $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') | ||||
|             ->setParameter('id', $destinationStoredObject->getId()) | ||||
|             ->execute(); | ||||
|  | ||||
|         $this->generator->generateDocFromTemplate( | ||||
|         try { | ||||
|             if ($message->isDumpOnly()) { | ||||
|                 $destinationStoredObject = $this->generator->generateDataDump( | ||||
|                     $template, | ||||
|                     $message->getEntityId(), | ||||
|                     $message->getContextGenerationData(), | ||||
|                     $destinationStoredObject, | ||||
|             false, | ||||
|             null, | ||||
|                     $creator | ||||
|                 ); | ||||
|  | ||||
|                 $this->sendDataDump($destinationStoredObject, $message); | ||||
|             } else { | ||||
|                 $destinationStoredObject = $this->generator->generateDocFromTemplate( | ||||
|                     $template, | ||||
|                     $message->getEntityId(), | ||||
|                     $message->getContextGenerationData(), | ||||
|                     $destinationStoredObject, | ||||
|                     $creator | ||||
|                 ); | ||||
|             } | ||||
|         } catch (StoredObjectManagerException|GeneratorException $e) { | ||||
|             $this->entityManager->flush(); | ||||
|  | ||||
|             $this->logger->error(self::LOG_PREFIX.'Request generation failed', [ | ||||
|                 'template_id' => $message->getTemplateId(), | ||||
|                 'destination_stored_object' => $message->getDestinationStoredObjectId(), | ||||
|                 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), | ||||
|                 'error' => $e->getTraceAsString(), | ||||
|             ]); | ||||
|  | ||||
|             throw $e; | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Request generation finished', [ | ||||
|             'template_id' => $message->getTemplateId(), | ||||
|             'destination_stored_object' => $message->getDestinationStoredObjectId(), | ||||
|             'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void | ||||
|     { | ||||
|         $url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600); | ||||
|         $parts = []; | ||||
|         parse_str(parse_url((string) $url->url)['query'], $parts); | ||||
|         $validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']); | ||||
|  | ||||
|         $email = (new TemplatedEmail()) | ||||
|             ->to($message->getSendResultToEmail()) | ||||
|             ->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig') | ||||
|             ->context([ | ||||
|                 'link' => $url->url, | ||||
|                 'validity' => $validity, | ||||
|             ]) | ||||
|             ->subject($this->translator->trans('docgen.data_dump_email.subject')); | ||||
|  | ||||
|         $this->mailer->send($email); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\MainBundle\Entity\User; | ||||
|  | ||||
| class RequestGenerationMessage | ||||
| final readonly class RequestGenerationMessage | ||||
| { | ||||
|     private readonly int $creatorId; | ||||
|     private int $creatorId; | ||||
|  | ||||
|     private readonly int $templateId; | ||||
|     private int $templateId; | ||||
|  | ||||
|     private readonly int $destinationStoredObjectId; | ||||
|     private int $destinationStoredObjectId; | ||||
|  | ||||
|     private readonly \DateTimeImmutable $createdAt; | ||||
|     private \DateTimeImmutable $createdAt; | ||||
|  | ||||
|     private ?string $sendResultToEmail; | ||||
|  | ||||
|     public function __construct( | ||||
|         User $creator, | ||||
|         DocGeneratorTemplate $template, | ||||
|         private readonly int $entityId, | ||||
|         private int $entityId, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         private readonly array $contextGenerationData | ||||
|         private array $contextGenerationData, | ||||
|         private bool $isTest = false, | ||||
|         ?string $sendResultToEmail = null, | ||||
|         private bool $dumpOnly = false, | ||||
|     ) { | ||||
|         $this->creatorId = $creator->getId(); | ||||
|         $this->templateId = $template->getId(); | ||||
|         $this->destinationStoredObjectId = $destinationStoredObject->getId(); | ||||
|         $this->createdAt = new \DateTimeImmutable('now'); | ||||
|         $this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail(); | ||||
|     } | ||||
|  | ||||
|     public function getCreatorId(): int | ||||
| @@ -67,4 +73,19 @@ class RequestGenerationMessage | ||||
|     { | ||||
|         return $this->createdAt; | ||||
|     } | ||||
|  | ||||
|     public function isTest(): bool | ||||
|     { | ||||
|         return $this->isTest; | ||||
|     } | ||||
|  | ||||
|     public function getSendResultToEmail(): ?string | ||||
|     { | ||||
|         return $this->sendResultToEmail; | ||||
|     } | ||||
|  | ||||
|     public function isDumpOnly(): bool | ||||
|     { | ||||
|         return $this->dumpOnly; | ||||
|     } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user