mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Compare commits
	
		
			271 Commits
		
	
	
		
			405-aside-
			...
			testing-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cf36070c82 | |||
| 8ee836eec3 | |||
| 388030952b | |||
| 82a08f1e27 | |||
| 925fbaed6d | |||
| c407c3029f | |||
| 81b6ae193c | |||
| 51168ac3c4 | |||
| 12ee091d09 | |||
| 49607e431f | |||
| ea4cbfe3b9 | |||
| 9d00b8ae60 | |||
| 00350b9efc | |||
| 9a50dad671 | |||
| 4206d17345 | |||
| 110a2e894f | |||
| a0daf4428f | |||
| f78b8cad9c | |||
| 6b0c85cdf0 | |||
| 92b71af239 | |||
| a0db7cd7e6 | |||
| 011b6a29e4 | |||
| 527dc971d7 | |||
| 43e5bc8337 | |||
| 845e582c44 | |||
| f3e04bd2bf | |||
| 96e95dd8f1 | |||
| c40e790425 | |||
| 3a016aa12a | |||
| be448c650e | |||
| 9f32b5ac48 | |||
| e89f5e4713 | |||
| e79d6d670b | |||
| 9adbde0308 | |||
| fe31cfd544 | |||
| e8bca6a502 | |||
| b0918ddd09 | |||
| a368e68abb | |||
| 85b9784eef | |||
| 26fd16ab07 | |||
| 9a3fef862e | |||
| 2c01516f71 | |||
| 246546b313 | |||
| bfe658d4fd | |||
| 7ea6638c3a | |||
| b04f0a9aa4 | |||
| ebdfa04843 | |||
| be213d5b5c | |||
| 4d1032c115 | |||
| e43dfc9a20 | |||
| b11684fb3b | |||
| b8826c6c0f | |||
| a996b05ead | |||
| cfcecf1cdc | |||
| b6985e0e5f | |||
| 6d76b94644 | |||
| ff6ec45575 | |||
| f3fd18e6fb | |||
| c8851a8e8a | |||
| abdfe49c33 | |||
| e933f3e781 | |||
| fb1c34f9c1 | |||
| d506409d93 | |||
| 9be8a533ff | |||
| b414b27ba9 | |||
| 8521672660 | |||
| 3f5ce5f841 | |||
| e1404bf16d | |||
| 65b7ed0755 | |||
| a74118e5d4 | |||
| 828739edf5 | |||
| d0811c8118 | |||
| 176bff0551 | |||
| 66c089e862 | |||
| aa44577484 | |||
| 8c59cbc6a0 | |||
| 8c5a7ac3e1 | |||
| a6e523ee0a | |||
| d49058805a | |||
| 73496e0e1f | |||
| 973450110b | |||
| 0f6b10aa0a | |||
| edeb8edbea | |||
| fc8e3789e0 | |||
| 52a80f9621 | |||
| 5632697c05 | |||
| 75932b0e29 | |||
| 7e2bf91e09 | |||
| 0581b59dbd | |||
| 4661ba9932 | |||
| f85b0c8cf7 | |||
| e701e96187 | |||
| d9d151aa89 | |||
| a14ed78e25 | |||
| 420dd4f868 | |||
| 3d9b9ea672 | |||
| 9f12b42961 | |||
| 2842548c17 | |||
| 35f5501489 | |||
| 5e2d960a19 | |||
| 4129283a58 | |||
| f807d62dab | |||
| 5a7bba83f7 | |||
| 50c75dff1a | |||
| e37f3e7c37 | |||
| b6375cad6c | |||
| 8516c87a14 | |||
| 8c5abbff74 | |||
| e0f94ae900 | |||
| f683e45b6e | |||
| b1b7fb6401 | |||
| 8fa06e143d | |||
| ec3d901d2f | |||
| f12d689382 | |||
| 1c04219859 | |||
| 2b88593e64 | |||
| ee65c46d2a | |||
| 7c239eaf6a | |||
| 694b1f3c1f | |||
| 80b9ce3c3e | |||
| 3f218e7183 | |||
| a2713041da | |||
| 3a904e8ea1 | |||
| 6f1a26742d | |||
| 0d0a626f50 | |||
| ec5f4ed1d6 | |||
| d9251239f7 | |||
| b2d3d806b6 | |||
| 8e952cc966 | |||
| 1c4ee37507 | |||
| 8331a836f2 | |||
| 2482dcc62e | |||
| f9a55a1bfd | |||
| 15aa565caf | |||
| 566b72ec9e | |||
| bb42ee25ff | |||
| aebeca1d7a | |||
| 1955249a60 | |||
| 3b0a4e9c73 | |||
| b5fd9cf4af | |||
| bb30ddc876 | |||
| 5ebb53173e | |||
| d1d6a00ebf | |||
| e48bec490c | |||
| 128d365a72 | |||
| 10f66afdcd | |||
| 89aed74355 | |||
| b3bf405c5b | |||
| 9124fa68e8 | |||
| 68b61b7d8a | |||
| 80d8f967fa | |||
| 1375a41de2 | |||
| 4fa4d3b65c | |||
| bd4c34cc1d | |||
| 4cea678e93 | |||
| 5e6833975b | |||
| f523b9adb3 | |||
| a211549432 | |||
| 17b1363113 | |||
| 3356ed8e57 | |||
| 2a7fa517ee | |||
| 95972399a1 | |||
| 85781c8e14 | |||
| 00eb435896 | |||
| ed71cffd6a | |||
| ae679e6997 | |||
| e1d308fd97 | |||
| d9acda67e3 | |||
| e88da74882 | |||
| 591c44d1a0 | |||
| bf04b7981c | |||
| df33eec30f | |||
| c657c98918 | |||
| ef5eb5b907 | |||
| d683fe002d | |||
| 555bbca59b | |||
| e9e9d5c458 | |||
| b1842a33ae | |||
| 6afeaccf24 | |||
| fb76bac480 | |||
| 6ded185289 | |||
| 95adc29f9d | |||
| 4d0c3e683f | |||
| 018aafc773 | |||
| c4aea4efc2 | |||
| 225e3ca13f | |||
| 8c1fa7956a | |||
| e253d1b276 | |||
| a52aac2d98 | |||
| 9e8cf60dd8 | |||
| 7682d81d50 | |||
| 5d31ce96c1 | |||
| 81ef64a246 | |||
| 49d1f78001 | |||
| 0d0f3528e2 | |||
| d97d5e689a | |||
| 95d80ce13e | |||
| 668720984d | |||
| 245c3fa121 | |||
| 1af3e4c7ec | |||
| 5d0fc7a189 | |||
| 483d50e776 | |||
| b4c6ccf309 | |||
| ac6a81cbd8 | |||
| 9503cb89b6 | |||
| b130dbdcdc | |||
| b2b1865837 | |||
| ec5c4d51b3 | |||
| 180437f637 | |||
| 0c2508d26d | |||
| b2d8d21f04 | |||
| 6a2aa77ecc | |||
| e7cd9e00f9 | |||
| 39f60b5b34 | |||
| c9c29b9105 | |||
| fb806a9579 | |||
| 70ca4acafb | |||
| bd61eedfbb | |||
| 80ce7f0bf1 | |||
| 1ebf838bde | |||
| 85a9c6bb67 | |||
| db073fc920 | |||
| 46ebfca28f | |||
| 22a2605381 | |||
| 4f6a7116a4 | |||
| cac7d33a44 | |||
| 0609e3f4c3 | |||
| 3a738179f2 | |||
| a789bf5e1c | |||
| 0a21fada42 | |||
| 4f030eb11a | |||
| 0d2a487ae7 | |||
| 6cb23344fc | |||
| da0d7a3b9e | |||
| fcc61aa4c0 | |||
| 21ec96b75c | |||
| 93f934152f | |||
| 49f4cce72a | |||
| e69b679938 | |||
| 229f9b7125 | |||
| 4fc433cd63 | |||
| 2c91d2e10f | |||
| 4302506471 | |||
| 09b7558e92 | |||
| 6ba5c91ee6 | |||
| 535409e529 | |||
| 9979378e78 | |||
| 58291c7402 | |||
| 3d397c0145 | |||
| d84f3ee5ad | |||
| 6db16e6d0b | |||
| ed60c6aaa3 | |||
| 13b1d20ade | |||
| 5999c73c98 | |||
| 580366de6d | |||
| 7f69f21b64 | |||
| 683a0bc4e9 | |||
| 4d6d40629f | |||
| 2185791665 | |||
| bf14c92567 | |||
| 7c1c1ed800 | |||
| 3b3659f13f | |||
| ebfdc57fcf | |||
| a562690512 | |||
| 1751c65731 | |||
| 791f5bb4be | |||
| 2c812fc5fe | |||
| 1f1d38acef | |||
| 057c34610d | |||
| 732b7dc8f7 | |||
| cb068cdee7 | 
							
								
								
									
										6
									
								
								.changes/unreleased/DX-20250401-144728.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/DX-20250401-144728.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: DX | ||||
| body: Allow TranslatableMessage in flash messages | ||||
| time: 2025-04-01T14:47:28.814268801+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										44
									
								
								.changes/unreleased/DX-20250407-121010.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.changes/unreleased/DX-20250407-121010.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| kind: DX | ||||
| body: | | ||||
|     Rewrite exports to run them asynchronously | ||||
|  | ||||
| changelog: | | ||||
|     - Add new methods to serialize data using the rector rule | ||||
|     - Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`. | ||||
|     - Refactor manually the initializeQuery method | ||||
|     - Remove the injection of ExportManager into the constructor of each export element: | ||||
|  | ||||
|       ```diff | ||||
|  | ||||
|       - class MyFormatter implements FormatterInterface | ||||
|       + class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface | ||||
|       { | ||||
|       +    use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; | ||||
|  | ||||
|       -    public function __construct(private ExportManager $exportmanager) {} | ||||
|  | ||||
|            public function MyMethod(): void | ||||
|            { | ||||
|       -          $this->exportManager->getFilter('alias'); | ||||
|       +          $this->getExportManager()->getFilter('alias'); | ||||
|            } | ||||
|       } | ||||
|       ``` | ||||
|     - configure messenger to handle export in a queue: | ||||
|  | ||||
|     ```diff | ||||
|     # config/packages/messenger.yaml | ||||
|     framework: | ||||
|         messenger: | ||||
|             routing: | ||||
|     +            'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority | ||||
|     ``` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| time: 2025-04-07T12:10:10.682561327+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: Add columns or tables | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/DX-20250430-144550.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/DX-20250430-144550.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: DX | ||||
| body: Remove dead code for wopi-link module | ||||
| time: 2025-04-30T14:45:50.406111606+02:00 | ||||
| custom: | ||||
|     Issue: "352" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/DX-20250528-165813.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/DX-20250528-165813.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: DX | ||||
| body: Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required) | ||||
| time: 2025-05-28T16:58:13.226870341+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Allow the merge of two accompanying period works | ||||
| time: 2025-02-11T14:22:43.134106669+01:00 | ||||
| custom: | ||||
|     Issue: "359" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Feature-20250424-142211.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Feature-20250424-142211.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Feature | ||||
| body: Add the document file name to the document title when a user upload a document, | ||||
|   unless there is already a document title. | ||||
| time: 2025-04-24T14:22:11.800975422+02:00 | ||||
| custom: | ||||
|   Issue: "377" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250520-095628.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250520-095628.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Add desactivation date for social action and issue csv export | ||||
| time: 2025-05-20T09:56:28.108941934+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250523-133341.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250523-133341.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Add Emoji and Fullscreen feature to ckeditor configuration | ||||
| time: 2025-05-23T13:33:41.645095128+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250523-133434.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250523-133434.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Create editor which allow us to toggle between rich and simple text editor | ||||
| time: 2025-05-23T13:34:34.56795603+02:00 | ||||
| custom: | ||||
|     Issue: "321" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Fixed-20250424-133943.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Fixed-20250424-133943.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Fixed | ||||
| body: trying to prevent bug of typeerror in doc-history + improved display of document | ||||
|   history | ||||
| time: 2025-04-24T13:39:43.878468232+02:00 | ||||
| custom: | ||||
|   Issue: "376" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Fixed-20250424-163746.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Fixed-20250424-163746.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Fixed | ||||
| body: Display previous participation in acc course work even if the person has left | ||||
|   the acc course | ||||
| time: 2025-04-24T16:37:46.970203594+02:00 | ||||
| custom: | ||||
|   Issue: "381" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250505-102715.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250505-102715.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fix display of text in calendar events | ||||
| time: 2025-05-05T10:27:15.461493066+02:00 | ||||
| custom: | ||||
|     Issue: "372" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250514-145339.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250514-145339.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Add missing translation for user_group.no_user_groups | ||||
| time: 2025-05-14T14:53:39.53927329+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250520-140008.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250520-140008.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fix retrieve schema to form full tablename and construct sql statements correctly in Thirdparty merger. | ||||
| time: 2025-05-20T14:00:08.987229634+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250520-140433.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250520-140433.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fix add missing translation | ||||
| time: 2025-05-20T14:04:33.612140549+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250520-164429.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250520-164429.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fix the transfer of evaluations and documents during of accompanyingperiodwork | ||||
| time: 2025-05-20T16:44:29.093304653+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,6 +0,0 @@ | ||||
| kind: Fixed | ||||
| body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted | ||||
| time: 2025-10-03T22:40:44.685474863+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,6 +0,0 @@ | ||||
| kind: Fixed | ||||
| body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists' | ||||
| time: 2025-10-06T12:13:15.45905994+02:00 | ||||
| custom: | ||||
|     Issue: "434" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/UX-20250423-172624.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/UX-20250423-172624.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: UX | ||||
| body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page | ||||
| time: 2025-04-23T17:26:24.45777387+02:00 | ||||
| custom: | ||||
|     Issue: "374" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,22 +0,0 @@ | ||||
| ## v3.12.0 - 2025-06-30 | ||||
| ### Feature | ||||
| * ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.    | ||||
| * Add desactivation date for social action and issue csv export    | ||||
| * Add Emoji and Fullscreen feature to ckeditor configuration    | ||||
| * ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor    | ||||
| * Do not remove workflow which are automatically canceled after staling for more than 30 days    | ||||
| ### Fixed | ||||
| * ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history    | ||||
| * ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course    | ||||
| * ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events    | ||||
| * Add missing translation for user_group.no_user_groups    | ||||
| * Fix admin entity edit actions for event admin entities and activity reason (category) entities    | ||||
| * ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem | ||||
|     | ||||
| * ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.    | ||||
| ### DX | ||||
| * ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module    | ||||
| * Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)    | ||||
| ### UX | ||||
| * ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page    | ||||
| * Improve labeling of fields in person resource creation form    | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v3.12.1 - 2025-06-30 | ||||
| ### Fixed | ||||
| * Fix loading of the list of documents    | ||||
| @@ -1,74 +0,0 @@ | ||||
| ## v4.0.0 - 2025-07-08 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works | ||||
| ### Fixed | ||||
| * ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor | ||||
| * Fix admin entity edit actions for event admin entities and activity reason (category) entities | ||||
| * Fix translations for social action fields in admin form: results, goals, evaluations | ||||
| ### DX | ||||
| * Rewrite exports to run them asynchronously | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * Allow TranslatableMessage in flash messages | ||||
| ### UX | ||||
| * Improve labeling of fields in person resource creation form | ||||
|  | ||||
|  | ||||
| **Release notes** | ||||
|  | ||||
| - Add new methods to serialize data using the rector rule | ||||
| - Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`. | ||||
| - Refactor manually the initializeQuery method | ||||
| - Remove the injection of ExportManager into the constructor of each export element: | ||||
|  | ||||
|   ```diff | ||||
|  | ||||
|   - class MyFormatter implements FormatterInterface | ||||
|   + class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface | ||||
|   { | ||||
|   +    use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; | ||||
|  | ||||
|   -    public function __construct(private ExportManager $exportmanager) {} | ||||
|  | ||||
|        public function MyMethod(): void | ||||
|        { | ||||
|   -          $this->exportManager->getFilter('alias'); | ||||
|   +          $this->getExportManager()->getFilter('alias'); | ||||
|        } | ||||
|   } | ||||
|   ``` | ||||
| - configure messenger to handle export in a queue: | ||||
|  | ||||
| ```diff | ||||
| # config/packages/messenger.yaml | ||||
| framework: | ||||
|     messenger: | ||||
|         routing: | ||||
| +            'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority | ||||
| ``` | ||||
|  | ||||
| - add missing methods to exports, aggregators, filters, formatter: | ||||
|  | ||||
|   ```php | ||||
|   public function normalizeFormData(array $formData): array; | ||||
|  | ||||
|   public function denormalizeFormData(array $formData, int $fromVersion): array; | ||||
|   ``` | ||||
|  | ||||
|   There are rector rules to generate those methods: | ||||
|  | ||||
|   - `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector` | ||||
|  | ||||
|   See: | ||||
|  | ||||
|   ```php | ||||
|   // upgrade chill exports | ||||
|   $rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]); | ||||
|   ``` | ||||
|  | ||||
|   This rule will create most of the work necessary, but some manuals changes are still necessary: | ||||
|  | ||||
|   - we must set manually the correct repository for method `denormalizeDoctrineEntity`; | ||||
|   - when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`) | ||||
|  | ||||
|  | ||||
| @@ -1,4 +0,0 @@ | ||||
| ## v4.0.1 - 2025-07-08 | ||||
| ### Fixed | ||||
| * Fix package.json for compilation | ||||
|     | ||||
| @@ -1,4 +0,0 @@ | ||||
| ## v4.0.2 - 2025-07-09 | ||||
| ### Fixed | ||||
| * Fix add missing translation    | ||||
| * Fix the transfer of evaluations and documents during of accompanyingperiodwork    | ||||
| @@ -1,12 +0,0 @@ | ||||
| ## v4.1.0 - 2025-08-26 | ||||
| ### Feature | ||||
| * ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes    | ||||
| * ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default    | ||||
| * Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.    | ||||
| * ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType    | ||||
| ### UX | ||||
| * Limit display of participations in event list    | ||||
| @@ -1,10 +0,0 @@ | ||||
| ## v4.2.0 - 2025-09-02 | ||||
| ### Feature | ||||
| * ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email | ||||
| ### Fixed | ||||
| * ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password | ||||
| * Fix typo in 'uncheckAll' script for centers selection | ||||
| * Fix incorrect parameter name in event details link | ||||
| @@ -1,6 +0,0 @@ | ||||
| ## v4.2.1 - 2025-09-03 | ||||
| ### Fixed | ||||
| * Fix exports to work with DirectExportInterface    | ||||
| ### DX | ||||
| * Improve error message when a stored object cannot be written on local disk | ||||
|     | ||||
| @@ -1,10 +0,0 @@ | ||||
| ## v4.3.0 - 2025-09-08 | ||||
| ### Feature | ||||
| * ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges    | ||||
| * Add a command to generate a list of permissions    | ||||
| * ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * fix date formatting in calendar range display    | ||||
| * Change route URL to avoid clash with person duplicate controller method    | ||||
| @@ -1,8 +0,0 @@ | ||||
| ## v4.4.0 - 2025-09-11 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works    | ||||
| * ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation    | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works    | ||||
| ### Fixed | ||||
| * Fix display of 'duplicate' and 'merge' buttons in CRUD templates    | ||||
| * Fix saving notification preferences in user's profile    | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v4.4.1 - 2025-09-11 | ||||
| ### Fixed | ||||
| * fix translations in duplicate evaluation document modal and realign close modal button    | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
| @@ -1,13 +0,0 @@ | ||||
| ## v4.5.0 - 2025-10-03 | ||||
| ### Feature | ||||
| * Only allow delete of attachment on workflows that are not final    | ||||
| * Move up signature buttons on index workflow page for easier access    | ||||
| * Filter out document from attachment list if it is the same as the workflow document    | ||||
| * Block edition on attached document on workflow, if the workflow is finalized or sent external    | ||||
| * Convert workflow's attached document to pdf while sending them external    | ||||
| * After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition    | ||||
| ### Fixed | ||||
| * ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance    | ||||
| * Fix permissions on storedObject which are subject by a workflow    | ||||
| ### DX | ||||
| * Introduce a WaitingScreen component to display a waiting screen    | ||||
| @@ -1,4 +0,0 @@ | ||||
| ## v4.5.1 - 2025-10-03 | ||||
| ### Fixed | ||||
| * Add missing javascript dependency    | ||||
| * Add exception handling for conversion of attachment on sending external, when documens are already in pdf    | ||||
| @@ -7,6 +7,14 @@ | ||||
|         "message": "'app' is assigned a value but never used.", | ||||
|         "hash": "f8c2979921289906e3baabae31ba101ead91504f" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js", | ||||
|         "line": 57, | ||||
|         "column": 23, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'event' is defined but never used.", | ||||
|         "hash": "cf0cf378f71403f62a6425f384ccbbdec433d1f2" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/module/Invite/answer.js", | ||||
|         "line": 7, | ||||
| @@ -119,6 +127,46 @@ | ||||
|         "message": "'payload' is defined but never used.", | ||||
|         "hash": "66c545917093ba30f1d6ca10ddaa676140e749bd" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue", | ||||
|         "line": 224, | ||||
|         "column": 10, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reactive' is defined but never used.", | ||||
|         "hash": "96ed76a9828138fb125fc36c4b55e900bbfe87c2" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue", | ||||
|         "line": 230, | ||||
|         "column": 5, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'DropArg' is defined but never used.", | ||||
|         "hash": "bd405399a4091d65e8391404bfb0c4611816c8e0" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue", | ||||
|         "line": 251, | ||||
|         "column": 9, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'t' is assigned a value but never used.", | ||||
|         "hash": "bc09207a496405f7a71c178e522b89aeb1f7ebd3" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue", | ||||
|         "line": 356, | ||||
|         "column": 32, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'arg' is defined but never used.", | ||||
|         "hash": "aeae152f0669b946a1ad681dd52b0ef03393ae79" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue", | ||||
|         "line": 434, | ||||
|         "column": 11, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'changedEvent' is assigned a value but never used.", | ||||
|         "hash": "a7a81a6bf09d00c0364e3aa8207ffad853f0547b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue", | ||||
|         "line": 77, | ||||
| @@ -351,6 +399,14 @@ | ||||
|         "message": "'error' is defined but never used.", | ||||
|         "hash": "e26e5e101e90d2b7ee84d6f5de8c819e52129c17" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts", | ||||
|         "line": 29, | ||||
|         "column": 14, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'vm' is defined but never used.", | ||||
|         "hash": "8e7f5e89dd72c54459cf82156389b88988f97d63" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/uploader.js", | ||||
|         "line": 39, | ||||
| @@ -559,6 +615,14 @@ | ||||
|         "message": "'ref' is defined but never used.", | ||||
|         "hash": "2a27cd6d06a26e1326654c929068e3704137e24b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue", | ||||
|         "line": 57, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/valid-v-for", | ||||
|         "message": "Custom elements in iteration require 'v-bind:key' directives.", | ||||
|         "hash": "cce787939524e83dd135869e13738ef332d7156c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue", | ||||
|         "line": 15, | ||||
| @@ -919,6 +983,22 @@ | ||||
|         "message": "'_e' is defined but never used.", | ||||
|         "hash": "1d6448401778e8c56554020fe5abd47851ed33f3" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js", | ||||
|         "line": 21, | ||||
|         "column": 55, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'e' is defined but never used.", | ||||
|         "hash": "eae499e4f6e9f43a9d17f9cd917cb6d3d97be25c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js", | ||||
|         "line": 3, | ||||
|         "column": 55, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'e' is defined but never used.", | ||||
|         "hash": "088fd383e7807e484aefc9825209bc7c8942bd22" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js", | ||||
|         "line": 9, | ||||
| @@ -1009,19 +1089,115 @@ | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 516, | ||||
|         "column": 21, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"context\" prop.", | ||||
|         "hash": "984c4203f2ac1e1bb65f9ce76ecd03b763cfaa83" | ||||
|         "line": 247, | ||||
|         "column": 5, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'postAddressToPerson' is defined but never used.", | ||||
|         "hash": "8a41c437cf2b5554cbbe1704cd51f3102b3d5994" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 517, | ||||
|         "line": 248, | ||||
|         "column": 5, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'postAddressToHousehold' is defined but never used.", | ||||
|         "hash": "66dec84b2ece299daf21308e5e60d497ba442b27" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 490, | ||||
|         "column": 21, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"context\" prop.", | ||||
|         "hash": "c9fb019bc21bfa77d989ed596913b99dd653c594" | ||||
|         "hash": "0d3f40c47974a4371072b3b9ee04b197c830162d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 491, | ||||
|         "column": 21, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"context\" prop.", | ||||
|         "hash": "8e877b7e588c30e182f7b572bdb9685360f9cf99" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 508, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "5a3e3401bc3c765d91faaf4cfde57697af1262b7" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 525, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "35a741d90379574b9323279f5802193d0c98a9dc" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 553, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "c23d1ddf6c0d10ae97948e74aee9c14b9320b86c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 572, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "4322e81c6ea9d9734c680633a724d5bd4fabacb2" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 803, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "7928a6461b9d394c7d97f048933553936f7d8963" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", | ||||
|         "line": 852, | ||||
|         "column": 47, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "e5afdb8efccb5470a08dde48f755b1268fa947b5" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 93, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "68f5e1cf5c03f9ada59c9e0afca0b74c7f3fca4b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 101, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "50d730f6109092baff2db66adc44dc1315e2bda2" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 109, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "573e4c041ce663f28b933d7a675c2a525aba644c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 117, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "293f845eeab515b1df4649d136c2d8219ed59c4d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
| @@ -1048,180 +1224,204 @@ | ||||
|         "hash": "2d5a5e680ff207ad97c7e7b7d999064b561dfd8a" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 149, | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 106, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "e4c1ecd7ae77d46ac3625c5bbe92a24d6a964db9" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 157, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "4dece2db87c6ce1c04ae06c088ddfe916c1c0c61" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 165, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "facc7a0f17bdf19396fae3d0de3da82e60503c0d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", | ||||
|         "line": 173, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "19de32c76518387218264d7c4dab914d143a9cca" | ||||
|         "hash": "d52356f2af31d0167c02330ec22d09fbfa6b2b9f" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 130, | ||||
|         "line": 114, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "239ac02a02694d5b20ab30d4c7ce5838c51d1515" | ||||
|         "hash": "c8e8e06f370f93bf05867e93b5f037dfa46937b1" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 138, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "a54f9bc6d1edfa4df93c7dd7d409cfef3fccf99e" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 152, | ||||
|         "line": 128, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "74a5f664d18f3916ea908897fcd0291cb0128f29" | ||||
|         "hash": "9abaf71ca4b4f292b3b01e724d0a7733365e71f1" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 129, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "0b0743959778a9e3d93089b132608816ee4e6646" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 132, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "9759da7b7859b8ee8efaf74876430658ac6b6fe2" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 133, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "dba8be9a27ab74ec743b7d9e07c05d857b407dd3" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 134, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "9b1f5bce779aafc46b19d7a5d266eaa29f8f9be9" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 139, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "fe6fc4aea0994ba9da15b7c09d308842b67958cb" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 153, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "740ea5d793c7a34c9f352d8b333f3aa04cc80ee8" | ||||
|         "column": 55, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "bd0e024fcad2e3f4566f15293e3c25c840f6dd3e" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 156, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "af8aca18f0226a5988ed90d44d95e2d607bfb5e6" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 157, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "7bc2453017793ae20cd6c10005f941d384b59d84" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 158, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "571b4ee5f22358dd165ec59696bb3439b7c9ff6c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 163, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "cfcb5946c86e289fc61623a794284a5a272d02e8" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 178, | ||||
|         "line": 154, | ||||
|         "column": 37, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "0ec402e43cb08bf129e0737c0d2c4f6d0c7af8bd" | ||||
|         "hash": "596c4b180b926b7829f987384328bf5636cd367a" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 196, | ||||
|         "line": 171, | ||||
|         "column": 59, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "5b41d5f9b45da074fb7bbbbd45e0da501da72071" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 172, | ||||
|         "column": 41, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "ec178d33e067aac892e015002afb6f3a2ff98762" | ||||
|         "hash": "d92b92a25043244cca809bd129633b7e024e26b4" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 214, | ||||
|         "line": 190, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "c0f4e5454e672b6064eb9cf6c235c6810f7bfa80" | ||||
|         "hash": "dd9a85ea740742d620e864796f67c5bff834486d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 215, | ||||
|         "line": 191, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "e3dd840d2474f9865a45822872bf9ecfb15961d7" | ||||
|         "hash": "e3e59960d0d50709a57b336f66b586710b774892" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 216, | ||||
|         "line": 192, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "a32a60382b145cc7a4a7ebe01ec435b8e3103320" | ||||
|         "hash": "fe11b0e54396511e7b3b08615a78d22fc27e2fad" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", | ||||
|         "line": 246, | ||||
|         "line": 222, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "082447e5c731012f3acc282943502775dfd24797" | ||||
|         "hash": "63c14c2150c33ec701bc4a0ff94efde69537d490" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 118, | ||||
|         "line": 96, | ||||
|         "column": 20, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "d4fba4fe09af3c0937c0dd164928c8930c1591b5" | ||||
|         "hash": "d2a9fdaeef0e2810f480022d4c6f99e4f76a818e" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 118, | ||||
|         "line": 96, | ||||
|         "column": 20, | ||||
|         "ruleId": "vue/no-side-effects-in-computed-properties", | ||||
|         "message": "Unexpected side effect in \"cities\" computed property.", | ||||
|         "hash": "1113a114d5aaf9f32f442916d25458541c5af35c" | ||||
|         "hash": "dd92a60a9b1ebefeb9a90941d45326fbfa483733" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 102, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "04be01ab638ce01f568fb0216929e65e1175ca23" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 110, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "8619c8e0b63e87d09268832f90e4fba06b87e41f" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 124, | ||||
|         "column": 17, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "fa56a7c93583f0a9d0c2ecac10228c4f4fc1bc3a" | ||||
|         "hash": "281f918da00635079501418b1e6b2c05b62eb4a7" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 132, | ||||
|         "column": 17, | ||||
|         "line": 125, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "9fe87937ea67d1dae95fb3d44d4be0da2eba0905" | ||||
|         "hash": "c131b09fa67ab1d069f1d04a54582d6b0f206153" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 126, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "3d3a2a4add64c291b8f5f1cddd90a173cd6a819d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 131, | ||||
|         "column": 21, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "ed48f4988914d7897018a2e06830a97e6740b3e8" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 145, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "744f3a7610d4d6015e50e25149bceffd6c6e2763" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
| @@ -1241,139 +1441,115 @@ | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 148, | ||||
|         "column": 13, | ||||
|         "line": 149, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "ab4f478fbfbc954b8dff75176dcd432f9ff28cfc" | ||||
|         "hash": "1e7b1ad55866f708baaca72dfa4ff26d6f8e5d21" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 153, | ||||
|         "column": 21, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "1d907d149f9ddb62e32140a90efe9a74b3e71fef" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 167, | ||||
|         "line": 152, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "8aa37d2d4f011773e68838a2c88017875de563b5" | ||||
|         "hash": "84779331536ffceec8d4a8c5ca4307310b882549" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 168, | ||||
|         "line": 161, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "a4827a357e52a51fa9262319114d81a130296acf" | ||||
|         "hash": "0789999841be671a4d8ab080d6fdb679f843eb52" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 169, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "a4c9715664202949e3242b8d4aa4098288b46dc4" | ||||
|         "line": 170, | ||||
|         "column": 51, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "bbb17afa114f016e2058d90aa32d2a625804f0d1" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 171, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "f3e9e21e433e90ec7b615b8940d43c4177372b66" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 174, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "770b7a24cc24b380e88db47d62422c8e1ece2571" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 183, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "2aef3c519a9ec6abcfe7573989d3de19d5c4c752" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 193, | ||||
|         "column": 33, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "5d1f97e4d7d9f47399d312e8b9f95ef9e3843b8c" | ||||
|         "hash": "5fbe407ceceb37bff2ac800ceddd7942540132f1" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 190, | ||||
|         "column": 55, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'reject' is defined but never used.", | ||||
|         "hash": "e2af91def877befbabef8e93deba4c58a3ee2ded" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 191, | ||||
|         "column": 37, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "ee8544ee45681a650ed7d4918ae979685cdd8f0f" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 210, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "5d9d2217c8c7e6571bc9f72a98ea5b370edb4968" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 211, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "6e04619b373c23c91f6c36c2aad314ac16cdb697" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 212, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "39df045639a62f64ccdb03a80e286bc3ad772587" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 213, | ||||
|         "column": 37, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "c1df874f790ef0c036bf58ae8a8db1ee173685d4" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 232, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "476e6588a28ac9382e8b9d2e63a8babecd23bad8" | ||||
|         "hash": "c399a43fa797a8ce61c9d96a644a39cc84a387b7" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 233, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "6a0c82ba72d6d87217bf33a6ad8e40a4b81bc802" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 234, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "741d5af6c7d90041c0dc1c1df2e8699b80fca69a" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 235, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "c3ffd141f58d532663875cc5c7d338ed00db2a6d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", | ||||
|         "line": 267, | ||||
|         "line": 245, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "2700f258396516a2fe971618fafbcdf72cdda3ab" | ||||
|         "hash": "04337a07944caaa4819cfebcf29e1a7cbfdf248b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue", | ||||
|         "line": 94, | ||||
|         "line": 76, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "4be1b0592efa775092a91a1d744e16ce98bd216e" | ||||
|         "hash": "373a2e31f110d138c66d77f1faf5dc61545c55af" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue", | ||||
|         "line": 99, | ||||
|         "line": 81, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "19b54b6d76c30249d520a296f826eda9d6eb0668" | ||||
|         "hash": "421eb6a63224b4b1d81b216677a710c5c99ddee3" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/DatePane.vue", | ||||
| @@ -1393,19 +1569,19 @@ | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue", | ||||
|         "line": 169, | ||||
|         "line": 155, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "dcb7b34098062760ddbb849655a5bb3ca65c36d3" | ||||
|         "hash": "b3a822914fcb5e2fcf28efc331a45b9205002eeb" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue", | ||||
|         "line": 178, | ||||
|         "line": 164, | ||||
|         "column": 17, | ||||
|         "ruleId": "vue/no-mutating-props", | ||||
|         "message": "Unexpected mutation of \"entity\" prop.", | ||||
|         "hash": "86b3ecf201025cac36878c5e4bf8850fb9d58cb5" | ||||
|         "hash": "72c7d850f6cdeaf65b373a33234222f9766ee30b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js", | ||||
| @@ -1455,6 +1631,14 @@ | ||||
|         "message": "'app' is assigned a value but never used.", | ||||
|         "hash": "9e6125f4fc387dc362c69cc6e3ce360eb2851f1b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue", | ||||
|         "line": 60, | ||||
|         "column": 22, | ||||
|         "ruleId": "vue/require-valid-default-prop", | ||||
|         "message": "Type of the default value for 'suggested' prop must be a function.", | ||||
|         "hash": "d30212820bc2e97fa02d75dbc3a014558693f169" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue", | ||||
|         "line": 24, | ||||
| @@ -1543,6 +1727,14 @@ | ||||
|         "message": "'tags' is assigned a value but never used.", | ||||
|         "hash": "ae9bb2e0651c118ed9efd227e88b86cc83f5d80d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/StickyNav.vue", | ||||
|         "line": 116, | ||||
|         "column": 18, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'event' is defined but never used.", | ||||
|         "hash": "201f182769c6dfb87148b841e7d9b592be429669" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/index.js", | ||||
|         "line": 19, | ||||
| @@ -1575,6 +1767,14 @@ | ||||
|         "message": "'app' is assigned a value but never used.", | ||||
|         "hash": "aaaaa63e7a60443b8cbf8191feb9142852ebdf1c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue", | ||||
|         "line": 79, | ||||
|         "column": 13, | ||||
|         "ruleId": "vue/require-v-for-key", | ||||
|         "message": "Elements in iteration expect to have 'v-bind:key' directives.", | ||||
|         "hash": "422f53925922e59655d0f71624c19af75d41628c" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js", | ||||
|         "line": 12, | ||||
| @@ -1615,6 +1815,22 @@ | ||||
|         "message": "'evalFQDN' is assigned a value but never used.", | ||||
|         "hash": "7fc32caafa23addddf44f3acbc5045b4523a0271" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js", | ||||
|         "line": 611, | ||||
|         "column": 9, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'errors' is assigned a value but never used.", | ||||
|         "hash": "c41cf979fc1626c38328dbf1028800c3395496bd" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/App.vue", | ||||
|         "line": 282, | ||||
|         "column": 7, | ||||
|         "ruleId": "@typescript-eslint/no-unused-expressions", | ||||
|         "message": "Expected an assignment or function call and instead saw an expression.", | ||||
|         "hash": "de3a6e2bb10a80a2bacba665be74266c7efc7d64" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/index.js", | ||||
|         "line": 16, | ||||
| @@ -1631,6 +1847,38 @@ | ||||
|         "message": "'app' is assigned a value but never used.", | ||||
|         "hash": "2f161e663689e3e4dfe2c53b0d64c91a4d2b1a60" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue", | ||||
|         "line": 263, | ||||
|         "column": 19, | ||||
|         "ruleId": "vue/return-in-computed-property", | ||||
|         "message": "Expected to return a value in \"refreshNetwork\" computed property.", | ||||
|         "hash": "2c1b08a49098c83b09058cedc0a962126e91e544" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue", | ||||
|         "line": 270, | ||||
|         "column": 7, | ||||
|         "ruleId": "vue/no-side-effects-in-computed-properties", | ||||
|         "message": "Unexpected side effect in \"legendLayers\" computed property.", | ||||
|         "hash": "760948d2187c853f17ac9a1bd7107e883092d4f4" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue", | ||||
|         "line": 281, | ||||
|         "column": 5, | ||||
|         "ruleId": "vue/no-dupe-keys", | ||||
|         "message": "Duplicate key 'checkedLayers'. May cause name collision in script or template tag.", | ||||
|         "hash": "447edb461e15e3ff5c60c8ecba88131e442539aa" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue", | ||||
|         "line": 353, | ||||
|         "column": 7, | ||||
|         "ruleId": "@typescript-eslint/no-unused-expressions", | ||||
|         "message": "Expected an assignment or function call and instead saw an expression.", | ||||
|         "hash": "9cf656cbf1eb3d7cc0082e63adcd320b6093d14f" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js", | ||||
|         "line": 20, | ||||
| @@ -1639,6 +1887,22 @@ | ||||
|         "message": "'app' is assigned a value but never used.", | ||||
|         "hash": "9e94e6412b8a44e47bfe8e66218cad09cff5bed4" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriod/SetReferrer.vue", | ||||
|         "line": 42, | ||||
|         "column": 16, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'response' is defined but never used.", | ||||
|         "hash": "62de07b13c662e32332bb062038acee23978ea70" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue", | ||||
|         "line": 356, | ||||
|         "column": 28, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'_response' is defined but never used.", | ||||
|         "hash": "097e7788a2b5dea500b80b8a3cf968e57063a66a" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue", | ||||
|         "line": 6, | ||||
| @@ -1654,5 +1918,45 @@ | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'UserRenderBoxBadge' is defined but never used.", | ||||
|         "hash": "99eba0d8633b2c9497417f4f61ec4194dbb2a96b" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts", | ||||
|         "line": 4, | ||||
|         "column": 3, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'StoredObjectStatus' is defined but never used.", | ||||
|         "hash": "63f8c4572293916850d6165647774b27d4b732c6" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts", | ||||
|         "line": 5, | ||||
|         "column": 3, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'StoredObjectStatusChange' is defined but never used.", | ||||
|         "hash": "a87c178e3eb5999bf0f46b3fa1c6da77e1be08b9" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts", | ||||
|         "line": 30, | ||||
|         "column": 61, | ||||
|         "ruleId": "@typescript-eslint/no-unused-vars", | ||||
|         "message": "'e' is defined but never used.", | ||||
|         "hash": "02953121583f4f73742a19adab099ab63df9076e" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts", | ||||
|         "line": 31, | ||||
|         "column": 32, | ||||
|         "ruleId": "@typescript-eslint/no-explicit-any", | ||||
|         "message": "Unexpected any. Specify a different type.", | ||||
|         "hash": "af48e21a1651b6017ede882dab249c00a818a44d" | ||||
|     }, | ||||
|     { | ||||
|         "path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts", | ||||
|         "line": 37, | ||||
|         "column": 16, | ||||
|         "ruleId": "@typescript-eslint/no-explicit-any", | ||||
|         "message": "Unexpected any. Specify a different type.", | ||||
|         "hash": "7513ea552a0a649ce4ab93b6cf9d40bfef4f68d9" | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -18,9 +18,6 @@ migrations/* | ||||
| templates/* | ||||
| translations/* | ||||
|  | ||||
| # we allow developers to add customization on their installation, without commiting it | ||||
| config/packages/dev/* | ||||
|  | ||||
| ###> symfony/framework-bundle ### | ||||
| /.env.local | ||||
| /.env.local.php | ||||
|   | ||||
| @@ -22,16 +22,16 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i | ||||
| - **Backend**: PHP 8.3+, Symfony 5.4 | ||||
| - **Frontend**: JavaScript/TypeScript, Vue.js 3, Bootstrap 5 | ||||
| - **Build Tools**: Webpack Encore, Yarn | ||||
| - **Database**: PostgreSQL with materialized views. We do not support other databases. | ||||
| - **Database**: PostgreSQL with materialized views | ||||
| - **Other Services**: Redis, AMQP (RabbitMQ), SMTP | ||||
|  | ||||
| ## Project Structure | ||||
|  | ||||
| Note: This is a project that's existed for a long time, and throughout the years we've used multiple structures inside each bundle. When having the choice, the developers should choose the new structure. | ||||
| Note: This is a project which exists from a long time ago, and we found multiple structure inside each bundle. When having the choice, the developers should choose the new structure. | ||||
|  | ||||
| The project follows a standard Symfony bundle structure: | ||||
| - `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`. | ||||
| - each bundle comes with its own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way). | ||||
| - each bundle come with his own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside to the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way). | ||||
| - `/docs/`: Contains project documentation | ||||
|  | ||||
| Each bundle typically has the following structure: | ||||
| @@ -46,13 +46,13 @@ Each bundle typically has the following structure: | ||||
|  | ||||
| ### A special word about TicketBundle | ||||
|  | ||||
| The ticket bundle is developed using a kind of "Command" pattern. The controller fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory. | ||||
| The ticket bundle is developed using a kind of "Command" pattern. The controller fill a "Command", and a "CommandHandler" handle this command. They are savec in the `src/Bundle/ChillTicketBundle/src/Action` directory. | ||||
|  | ||||
| ## Development Guidelines | ||||
|  | ||||
| ### Building and Configuration Instructions | ||||
|  | ||||
| All the commands should be run through the `symfony` command, which will configure the required variables. | ||||
| All the command should be run through the `symfony` command, which will configure the required variables. | ||||
|  | ||||
| For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`. | ||||
|  | ||||
| @@ -87,7 +87,7 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u | ||||
|    docker compose up -d | ||||
|    ``` | ||||
|  | ||||
| 6. **Set Up the Database**: | ||||
| 5. **Set Up the Database**: | ||||
|    ```bash | ||||
|    # Create the database | ||||
|    symfony console doctrine:database:create | ||||
| @@ -99,20 +99,20 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u | ||||
|    symfony console doctrine:fixtures:load | ||||
|    ``` | ||||
|  | ||||
| 7. **Build Assets**: | ||||
| 6. **Build Assets**: | ||||
|    ```bash | ||||
|    nvm use 20 | ||||
|    yarn run encore dev | ||||
|    ``` | ||||
|  | ||||
| 8. **Start the Development Server**: | ||||
| 7. **Start the Development Server**: | ||||
|    ```bash | ||||
|    symfony server:start -d | ||||
|    ``` | ||||
|  | ||||
| #### Docker Setup | ||||
|  | ||||
| The project includes a Docker configuration for easier development: | ||||
| The project includes Docker configuration for easier development: | ||||
|  | ||||
| 1. **Start Docker Services**: | ||||
|    ```bash | ||||
| @@ -149,93 +149,14 @@ Key configuration files: | ||||
| - `package.json`: JavaScript dependencies and scripts | ||||
| - `.env`: Default environment variables. Must usually not be updated: use `.env.local` instead. | ||||
|  | ||||
| ### Database migrations | ||||
|  | ||||
| Each time a doctrine entity is created, we generate migration to adapt the database. | ||||
|  | ||||
| The migration is created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, remember to quote the `\` (`\` must become `\\` in your command). | ||||
|  | ||||
| Each bundle has his own namespace for migration (always ask me to confirm that command with a list of updated / created entities so that I can confirm to you that it is ok): | ||||
|  | ||||
| - `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`; | ||||
| - `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`; | ||||
| - `Chill\Bundle\CustomFieldsBundle` writes migrations to `Chill\Migrations\CustomFields`; | ||||
| - `Chill\Bundle\DocGeneratorBundle` writes migrations to `Chill\Migrations\DocGenerator`; | ||||
| - `Chill\Bundle\DocStoreBundle` writes migrations to `Chill\Migrations\DocStore`; | ||||
| - `Chill\Bundle\EventBundle` writes migrations to `Chill\Migrations\Event`; | ||||
| - `Chill\Bundle\CalendarBundle` writes migrations to `Chill\Migrations\Calendar`; | ||||
| - `Chill\Bundle\FamilyMembersBundle` writes migrations to `Chill\Migrations\FamilyMembers`; | ||||
| - `Chill\Bundle\FranceTravailApiBundle` writes migrations to `Chill\Migrations\FranceTravailApi`; | ||||
| - `Chill\Bundle\JobBundle` writes migrations to `Chill\Migrations\Job`; | ||||
| - `Chill\Bundle\MainBundle` writes migrations to `Chill\Migrations\Main`; | ||||
| - `Chill\Bundle\PersonBundle` writes migrations to `Chill\Migrations\Person`; | ||||
| - `Chill\Bundle\ReportBundle` writes migrations to `Chill\Migrations\Report`; | ||||
| - `Chill\Bundle\TaskBundle` writes migrations to `Chill\Migrations\Task`; | ||||
| - `Chill\Bundle\ThirdPartyBundle` writes migrations to `Chill\Migrations\ThirdParty`; | ||||
| - `Chill\Bundle\TicketBundle` writes migrations to `Chill\Migrations\Ticket`; | ||||
| - `Chill\Bundle\WopiBundle` writes migrations to `Chill\Migrations\Wopi`; | ||||
|  | ||||
| Once created the, comment's classes should be removed and a description of the changes made to the entities should be added to the migrations, using the `getDescription` method. The migration should not be cleaned by any artificial intelligence, as modifying this migration is error prone. | ||||
|  | ||||
| ### Guidelines related to code structure and requirements | ||||
|  | ||||
| #### Usage of clock | ||||
|  | ||||
| When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of | ||||
| `Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities, | ||||
| where injection does not work when restoring an entity from a database, but usually possible in services. | ||||
|  | ||||
| In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface` | ||||
| where we have full and easy control of the date. | ||||
|  | ||||
| ### Testing Information | ||||
|  | ||||
| The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level. | ||||
|  | ||||
| #### Use of mock in tests | ||||
|  | ||||
| ##### General mocking | ||||
|  | ||||
| For creating mock, we prefer using prophecy (library phpspec/prophecy). | ||||
|  | ||||
| ##### Useful helpers and tips that avoid creating a mock | ||||
|  | ||||
| Some notable implementations that are test helpers and avoid creating a mock: | ||||
|  | ||||
| - `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`; | ||||
| - `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above); | ||||
| - `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`; | ||||
| - When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport": | ||||
|  | ||||
|     ```php | ||||
|     use Symfony\Component\Mailer\Transport\InMemoryTransport; | ||||
|     use \Symfony\Component\Mailer\Mailer; | ||||
|  | ||||
|     $transport = new InMemoryTransport(); | ||||
|     $mailer = new Mailer($transport); | ||||
|  | ||||
|     // After sending: | ||||
|     $messages = $transport->getSent(); // array of SentMessage | ||||
|     ``` | ||||
| - When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`; | ||||
|  | ||||
| ##### When we prefer not creating a mock | ||||
|  | ||||
| - When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write); | ||||
|  | ||||
| ##### Mocking final and readonly classes | ||||
|  | ||||
| Classes marked as final can't be mocked. To avoid that, either: | ||||
|  | ||||
| - we remove the `final` keyword from the class; | ||||
| - we extract an interface from the final class. | ||||
|  | ||||
| This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case. | ||||
|  | ||||
| #### Running Tests | ||||
|  | ||||
| The tests are run from the project's root (not from the bundle's root). | ||||
|  | ||||
| ```bash | ||||
| # Run all tests | ||||
| vendor/bin/phpunit | ||||
| @@ -297,7 +218,7 @@ class TicketTest extends TestCase | ||||
|  | ||||
| #### Test Database | ||||
|  | ||||
| For tests that require a database, the project uses a postgresql database filled with fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file. | ||||
| For tests that require a database, the project uses an in-memory SQLite database by default. You can configure a different database for testing in the `.env.test` file. | ||||
|  | ||||
| ### Code Quality Tools | ||||
|  | ||||
|   | ||||
							
								
								
									
										210
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										210
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,216 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v4.5.1 - 2025-10-03 | ||||
| ### Fixed | ||||
| * Add missing javascript dependency    | ||||
| * Add exception handling for conversion of attachment on sending external, when documens are already in pdf    | ||||
|  | ||||
| ## v4.5.0 - 2025-10-03 | ||||
| ### Feature | ||||
| * Only allow delete of attachment on workflows that are not final    | ||||
| * Move up signature buttons on index workflow page for easier access    | ||||
| * Filter out document from attachment list if it is the same as the workflow document    | ||||
| * Block edition on attached document on workflow, if the workflow is finalized or sent external    | ||||
| * Convert workflow's attached document to pdf while sending them external    | ||||
| * After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition    | ||||
| ### Fixed | ||||
| * ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance    | ||||
| * Fix permissions on storedObject which are subject by a workflow    | ||||
| ### DX | ||||
| * Introduce a WaitingScreen component to display a waiting screen    | ||||
|  | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
|  | ||||
| ## v4.4.1 - 2025-09-11 | ||||
| ### Fixed | ||||
| * fix translations in duplicate evaluation document modal and realign close modal button    | ||||
|  | ||||
| ## v4.4.0 - 2025-09-11 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works    | ||||
| * ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation    | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works    | ||||
| ### Fixed | ||||
| * Fix display of 'duplicate' and 'merge' buttons in CRUD templates    | ||||
| * Fix saving notification preferences in user's profile    | ||||
|  | ||||
| ## v4.3.0 - 2025-09-08 | ||||
| ### Feature | ||||
| * ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges    | ||||
| * Add a command to generate a list of permissions    | ||||
| * ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * fix date formatting in calendar range display    | ||||
| * Change route URL to avoid clash with person duplicate controller method    | ||||
|  | ||||
| ## v4.2.1 - 2025-09-03 | ||||
| ### Fixed | ||||
| * Fix exports to work with DirectExportInterface    | ||||
| ### DX | ||||
| * Improve error message when a stored object cannot be written on local disk | ||||
|     | ||||
|  | ||||
| ## v4.2.0 - 2025-09-02 | ||||
| ### Feature | ||||
| * ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email | ||||
| ### Fixed | ||||
| * ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password | ||||
| * Fix typo in 'uncheckAll' script for centers selection | ||||
| * Fix incorrect parameter name in event details link | ||||
|  | ||||
| ## v4.1.0 - 2025-08-26 | ||||
| ### Feature | ||||
| * ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes    | ||||
| * ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default    | ||||
| * Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.    | ||||
| * ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType    | ||||
| ### UX | ||||
| * Limit display of participations in event list    | ||||
|  | ||||
| ## v4.0.2 - 2025-07-09 | ||||
| ### Fixed | ||||
| * Fix add missing translation    | ||||
| * Fix the transfer of evaluations and documents during of accompanyingperiodwork    | ||||
|  | ||||
| ## v4.0.1 - 2025-07-08 | ||||
| ### Fixed | ||||
| * Fix package.json for compilation | ||||
|     | ||||
|  | ||||
| ## v4.0.0 - 2025-07-08 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works | ||||
| ### Fixed | ||||
| * ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor | ||||
| * Fix admin entity edit actions for event admin entities and activity reason (category) entities | ||||
| * Fix translations for social action fields in admin form: results, goals, evaluations | ||||
| ### DX | ||||
| * Rewrite exports to run them asynchronously | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * Allow TranslatableMessage in flash messages | ||||
| ### UX | ||||
| * Improve labeling of fields in person resource creation form | ||||
|  | ||||
|  | ||||
| **Release notes** | ||||
|  | ||||
| - Add new methods to serialize data using the rector rule | ||||
| - Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`. | ||||
| - Refactor manually the initializeQuery method | ||||
| - Remove the injection of ExportManager into the constructor of each export element: | ||||
|  | ||||
|   ```diff | ||||
|  | ||||
|   - class MyFormatter implements FormatterInterface | ||||
|   + class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface | ||||
|   { | ||||
|   +    use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; | ||||
|  | ||||
|   -    public function __construct(private ExportManager $exportmanager) {} | ||||
|  | ||||
|        public function MyMethod(): void | ||||
|        { | ||||
|   -          $this->exportManager->getFilter('alias'); | ||||
|   +          $this->getExportManager()->getFilter('alias'); | ||||
|        } | ||||
|   } | ||||
|   ``` | ||||
| - configure messenger to handle export in a queue: | ||||
|  | ||||
| ```diff | ||||
| # config/packages/messenger.yaml | ||||
| framework: | ||||
|     messenger: | ||||
|         routing: | ||||
| +            'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority | ||||
| ``` | ||||
|  | ||||
| - add missing methods to exports, aggregators, filters, formatter: | ||||
|  | ||||
|   ```php | ||||
|   public function normalizeFormData(array $formData): array; | ||||
|  | ||||
|   public function denormalizeFormData(array $formData, int $fromVersion): array; | ||||
|   ``` | ||||
|  | ||||
|   There are rector rules to generate those methods: | ||||
|  | ||||
|   - `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector` | ||||
|  | ||||
|   See: | ||||
|  | ||||
|   ```php | ||||
|   // upgrade chill exports | ||||
|   $rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]); | ||||
|   ``` | ||||
|  | ||||
|   This rule will create most of the work necessary, but some manuals changes are still necessary: | ||||
|  | ||||
|   - we must set manually the correct repository for method `denormalizeDoctrineEntity`; | ||||
|   - when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`) | ||||
|  | ||||
|  | ||||
|  | ||||
| ## v3.12.1 - 2025-06-30 | ||||
| ### Fixed | ||||
| * Fix loading of the list of documents    | ||||
|  | ||||
| ## v3.12.0 - 2025-06-30 | ||||
| ### Feature | ||||
| * ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.    | ||||
| * Add desactivation date for social action and issue csv export    | ||||
| * Add Emoji and Fullscreen feature to ckeditor configuration    | ||||
| * ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor    | ||||
| * Do not remove workflow which are automatically canceled after staling for more than 30 days    | ||||
| ### Fixed | ||||
| * ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history    | ||||
| * ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course    | ||||
| * ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events    | ||||
| * Add missing translation for user_group.no_user_groups    | ||||
| * Fix admin entity edit actions for event admin entities and activity reason (category) entities    | ||||
| * ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem | ||||
|     | ||||
| * ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.    | ||||
| ### DX | ||||
| * ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module    | ||||
| * Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)    | ||||
| ### UX | ||||
| * ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page    | ||||
| * Improve labeling of fields in person resource creation form    | ||||
|  | ||||
| ## v3.11.0 - 2025-04-17 | ||||
| ### Feature | ||||
| * ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities. | ||||
| * ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * Signature: add a button to go directly to the signature zone, even if there is only one | ||||
| ### Fixed | ||||
| * Fixed wrong translations in the on-the-fly for creation of thirdParty | ||||
| * Fixed update of phone number in on-the-fly edition of thirdParty | ||||
| * Fixed closing of modal when editing thirdParty in accompanying course works | ||||
| * Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day | ||||
| * ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list | ||||
| * When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk | ||||
| * Add consistent log prefix and key to logs when stale workflows are automatically canceled | ||||
| * ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition | ||||
|  | ||||
| ### DX | ||||
| * Add new chill-col style for displaying title and aside in a flex table | ||||
|  | ||||
| ## v3.10.3 - 2025-03-18 | ||||
| ### DX | ||||
| * Eslint fixes    | ||||
|   | ||||
| @@ -1,2 +0,0 @@ | ||||
| chill_aside_activity: | ||||
|     show_concerned_persons_count: hidden | ||||
| @@ -62,10 +62,8 @@ framework: | ||||
|             'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority | ||||
|             'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async | ||||
|             'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async | ||||
|             'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async | ||||
|             'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority | ||||
|             'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async | ||||
|             'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async | ||||
|             # end of routes added by chill-bundles recipes | ||||
|             # Route your messages to the transports | ||||
|             # 'App\Message\YourMessage': async | ||||
|   | ||||
| @@ -55,7 +55,6 @@ | ||||
|     "@tsconfig/node20": "^20.1.4", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "@types/leaflet": "^1.9.3", | ||||
|     "@vueuse/core": "^13.9.0", | ||||
|     "bootstrap-icons": "^1.11.3", | ||||
|     "dropzone": "^5.7.6", | ||||
|     "es6-promise": "^4.2.8", | ||||
|   | ||||
| @@ -2154,6 +2154,11 @@ parameters: | ||||
| 			count: 1 | ||||
| 			path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php | ||||
|  | ||||
| 		- | ||||
| 			message: "#^Instanceof between string and DateTimeInterface will always evaluate to false\\.$#" | ||||
| 			count: 1 | ||||
| 			path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php | ||||
|  | ||||
| 		- | ||||
| 			message: "#^PHPDoc tag @var for property Chill\\\\MainBundle\\\\Export\\\\Helper\\\\ExportAddressHelper\\:\\:\\$unitNamesKeysCache contains unresolvable type\\.$#" | ||||
| 			count: 1 | ||||
|   | ||||
| @@ -37,6 +37,9 @@ return static function (RectorConfig $rectorConfig): void { | ||||
|     $rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class); | ||||
|     $rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class); | ||||
|  | ||||
|     // upgrade chill exports | ||||
|     $rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]); | ||||
|  | ||||
|     // part of the symfony 54 rules | ||||
|     $rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); | ||||
|     $rectorConfig->rule(\Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class); | ||||
|   | ||||
| @@ -48,6 +48,28 @@ class ActivityReasonCategoryController extends AbstractController | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Displays a form to edit an existing ActivityReasonCategory entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/edit', name: 'chill_activity_activityreasoncategory_edit')] | ||||
|     public function editAction(mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
|  | ||||
|         $entity = $em->getRepository(ActivityReasonCategory::class)->find($id); | ||||
|  | ||||
|         if (!$entity) { | ||||
|             throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.'); | ||||
|         } | ||||
|  | ||||
|         $editForm = $this->createEditForm($entity); | ||||
|  | ||||
|         return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [ | ||||
|             'entity' => $entity, | ||||
|             'edit_form' => $editForm->createView(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lists all ActivityReasonCategory entities. | ||||
|      */ | ||||
| @@ -78,10 +100,29 @@ class ActivityReasonCategoryController extends AbstractController | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Finds and displays a ActivityReasonCategory entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/show', name: 'chill_activity_activityreasoncategory_show')] | ||||
|     public function showAction(mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
|  | ||||
|         $entity = $em->getRepository(ActivityReasonCategory::class)->find($id); | ||||
|  | ||||
|         if (!$entity) { | ||||
|             throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.'); | ||||
|         } | ||||
|  | ||||
|         return $this->render('@ChillActivity/ActivityReasonCategory/show.html.twig', [ | ||||
|             'entity' => $entity, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Edits an existing ActivityReasonCategory entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update')] | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update', methods: ['POST', 'PUT'])] | ||||
|     public function updateAction(Request $request, mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
| @@ -98,7 +139,7 @@ class ActivityReasonCategoryController extends AbstractController | ||||
|         if ($editForm->isSubmitted() && $editForm->isValid()) { | ||||
|             $em->flush(); | ||||
|  | ||||
|             return $this->redirectToRoute('chill_activity_activityreasoncategory', ['id' => $id]); | ||||
|             return $this->redirectToRoute('chill_activity_activityreasoncategory_edit', ['id' => $id]); | ||||
|         } | ||||
|  | ||||
|         return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [ | ||||
| @@ -137,7 +178,7 @@ class ActivityReasonCategoryController extends AbstractController | ||||
|     { | ||||
|         $form = $this->createForm(ActivityReasonCategoryType::class, $entity, [ | ||||
|             'action' => $this->generateUrl('chill_activity_activityreasoncategory_update', ['id' => $entity->getId()]), | ||||
|             'method' => 'POST', | ||||
|             'method' => 'PUT', | ||||
|         ]); | ||||
|  | ||||
|         $form->add('submit', SubmitType::class, ['label' => 'Update']); | ||||
|   | ||||
| @@ -17,6 +17,7 @@ use Chill\ActivityBundle\Repository\ActivityReasonRepository; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||
|  | ||||
| /** | ||||
|  * ActivityReason controller. | ||||
| @@ -49,6 +50,28 @@ class ActivityReasonController extends AbstractController | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Displays a form to edit an existing ActivityReason entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/edit', name: 'chill_activity_activityreason_edit')] | ||||
|     public function editAction(mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
|  | ||||
|         $entity = $em->getRepository(ActivityReason::class)->find($id); | ||||
|  | ||||
|         if (null === $entity) { | ||||
|             throw new NotFoundHttpException('Unable to find ActivityReason entity.'); | ||||
|         } | ||||
|  | ||||
|         $editForm = $this->createEditForm($entity); | ||||
|  | ||||
|         return $this->render('@ChillActivity/ActivityReason/edit.html.twig', [ | ||||
|             'entity' => $entity, | ||||
|             'edit_form' => $editForm->createView(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lists all ActivityReason entities. | ||||
|      */ | ||||
| @@ -79,10 +102,29 @@ class ActivityReasonController extends AbstractController | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Finds and displays a ActivityReason entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/show', name: 'chill_activity_activityreason_show')] | ||||
|     public function showAction(mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
|  | ||||
|         $entity = $em->getRepository(ActivityReason::class)->find($id); | ||||
|  | ||||
|         if (!$entity) { | ||||
|             throw $this->createNotFoundException('Unable to find ActivityReason entity.'); | ||||
|         } | ||||
|  | ||||
|         return $this->render('@ChillActivity/ActivityReason/show.html.twig', [ | ||||
|             'entity' => $entity, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Edits an existing ActivityReason entity. | ||||
|      */ | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update')] | ||||
|     #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update', methods: ['POST', 'PUT'])] | ||||
|     public function updateAction(Request $request, mixed $id) | ||||
|     { | ||||
|         $em = $this->managerRegistry->getManager(); | ||||
| @@ -138,7 +180,7 @@ class ActivityReasonController extends AbstractController | ||||
|     { | ||||
|         $form = $this->createForm(ActivityReasonType::class, $entity, [ | ||||
|             'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]), | ||||
|             'method' => 'POST', | ||||
|             'method' => 'PUT', | ||||
|         ]); | ||||
|  | ||||
|         $form->add('submit', SubmitType::class, ['label' => 'Update']); | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import "es6-promise/auto"; | ||||
| import { createStore } from "vuex"; | ||||
| import { postLocation } from "./api"; | ||||
| import prepareLocations from "./store.locations.js"; | ||||
| import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods"; | ||||
| import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods"; | ||||
|  | ||||
| const debug = process.env.NODE_ENV !== "production"; | ||||
| //console.log('window.activity', window.activity); | ||||
| @@ -369,7 +369,7 @@ const store = createStore({ | ||||
|         // console.log('works', works); | ||||
|         commit("setAccompanyingPeriodWorks", works); | ||||
|       } catch (error) { | ||||
|         console.error("Failed to fetch works:", error); | ||||
|         console.error('Failed to fetch works:', error); | ||||
|       } | ||||
|     }, | ||||
|     getWhoAmI({ commit }) { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| {% block admin_content %} | ||||
|     <h1>{{ 'ActivityReason list'|trans }}</h1> | ||||
|  | ||||
|     <table class="table table-bordered border-dark align-middle"> | ||||
|     <table class="records_list"> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>{{ 'Name'|trans }}</th> | ||||
| @@ -29,7 +29,10 @@ | ||||
|                 <td> | ||||
|                     <ul class="record_actions"> | ||||
|                         <li> | ||||
|                             <a href="{{ path('chill_activity_activityreason_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> | ||||
|                             <a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> | ||||
|                         </li> | ||||
|                     </ul> | ||||
|                 </td> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| {% block admin_content %} | ||||
|     <h1>{{ 'ActivityReasonCategory list'|trans }}</h1> | ||||
|  | ||||
|     <table class="table table-bordered border-dark align-middle"> | ||||
|     <table class="records_list"> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>{{ 'Name'|trans }}</th> | ||||
| @@ -23,7 +23,10 @@ | ||||
|                 <td> | ||||
|                     <ul class="record_actions"> | ||||
|                     <li> | ||||
|                         <a href="{{ path('chill_activity_activityreasoncategory_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> | ||||
|                         <a href="{{ path('chill_activity_activityreasoncategory_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <a href="{{ path('chill_activity_activityreasoncategory_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|                 </td> | ||||
|   | ||||
| @@ -22,52 +22,6 @@ use Symfony\Component\Security\Core\Role\Role; | ||||
|  */ | ||||
| final class ActivityControllerTest extends WebTestCase | ||||
| { | ||||
|     /** | ||||
|      * @dataProvider getSecuredPagesUnauthenticated | ||||
|      */ | ||||
|     public function testAccessIsDeniedForUnauthenticated(mixed $url) | ||||
|     { | ||||
|         $client = $this->createClient(); | ||||
|  | ||||
|         $client->request('GET', $url); | ||||
|  | ||||
|         $this->assertEquals(302, $client->getResponse()->getStatusCode()); | ||||
|         $this->assertTrue( | ||||
|             $client->getResponse()->isRedirect('http://localhost/login'), | ||||
|             sprintf('the page "%s" does not redirect to http://localhost/login', $url) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Provide a client unauthenticated and. | ||||
|      */ | ||||
|     public function getSecuredPagesUnauthenticated() | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $person = $this->getPersonFromFixtures(); | ||||
|         $activities = $this->getActivitiesForPerson($person); | ||||
|  | ||||
|         return [ | ||||
|             [sprintf('fr/person/%d/activity/', $person->getId())], | ||||
|             [sprintf('fr/person/%d/activity/new', $person->getId())], | ||||
|             [sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())], | ||||
|             [sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getSecuredPagesAuthenticated | ||||
|      * | ||||
|      * @param type $client | ||||
|      * @param type $url | ||||
|      */ | ||||
|     public function testAccessIsDeniedForUnauthorized($client, $url) | ||||
|     { | ||||
|         $client->request('GET', $url); | ||||
|  | ||||
|         $this->assertEquals(403, $client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     public function getSecuredPagesAuthenticated() | ||||
|     { | ||||
|         self::bootKernel(); | ||||
| @@ -101,6 +55,52 @@ final class ActivityControllerTest extends WebTestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Provide a client unauthenticated and. | ||||
|      */ | ||||
|     public function getSecuredPagesUnauthenticated() | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $person = $this->getPersonFromFixtures(); | ||||
|         $activities = $this->getActivitiesForPerson($person); | ||||
|  | ||||
|         return [ | ||||
|             [sprintf('fr/person/%d/activity/', $person->getId())], | ||||
|             [sprintf('fr/person/%d/activity/new', $person->getId())], | ||||
|             [sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())], | ||||
|             [sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getSecuredPagesUnauthenticated | ||||
|      */ | ||||
|     public function testAccessIsDeniedForUnauthenticated(mixed $url) | ||||
|     { | ||||
|         $client = $this->createClient(); | ||||
|  | ||||
|         $client->request('GET', $url); | ||||
|  | ||||
|         $this->assertEquals(302, $client->getResponse()->getStatusCode()); | ||||
|         $this->assertTrue( | ||||
|             $client->getResponse()->isRedirect('http://localhost/login'), | ||||
|             sprintf('the page "%s" does not redirect to http://localhost/login', $url) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getSecuredPagesAuthenticated | ||||
|      * | ||||
|      * @param type $client | ||||
|      * @param type $url | ||||
|      */ | ||||
|     public function testAccessIsDeniedForUnauthorized($client, $url) | ||||
|     { | ||||
|         $client->request('GET', $url); | ||||
|  | ||||
|         $this->assertEquals(403, $client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     public function testCompleteScenario() | ||||
|     { | ||||
|         // Create a new client to browse the application | ||||
|   | ||||
| @@ -137,64 +137,6 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $period = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException('no period found'); | ||||
|         } | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException('no types'); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             $job = new UserJob(); | ||||
|             $job->setLabel(['fr' => 'test']); | ||||
|             $this->entityManager->persist($job); | ||||
|             $this->entityManager->flush(); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException('no user found'); | ||||
|         } | ||||
|  | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByPerson | ||||
|      */ | ||||
| @@ -349,4 +291,62 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $period = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException('no period found'); | ||||
|         } | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException('no types'); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             $job = new UserJob(); | ||||
|             $job->setLabel(['fr' => 'test']); | ||||
|             $this->entityManager->persist($job); | ||||
|             $this->entityManager->flush(); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException('no user found'); | ||||
|         } | ||||
|  | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -57,46 +57,6 @@ final class ActivityVoterTest extends KernelTestCase | ||||
|         $this->prophet = new \Prophecy\Prophet(); | ||||
|     } | ||||
|  | ||||
|     public function testNullUser() | ||||
|     { | ||||
|         $token = $this->prepareToken(); | ||||
|         $center = $this->prepareCenter(1, 'center'); | ||||
|         $person = $this->preparePerson($center); | ||||
|         $scope = $this->prepareScope(1, 'default'); | ||||
|         $activity = $this->prepareActivity($scope, $person); | ||||
|  | ||||
|         $this->assertEquals( | ||||
|             VoterInterface::ACCESS_DENIED, | ||||
|             $this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']), | ||||
|             'assert that a null user is not allowed to see' | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider dataProvider_testVoteAction | ||||
|      * | ||||
|      * @param type   $expectedResult | ||||
|      * @param string $attribute | ||||
|      * @param string $message | ||||
|      */ | ||||
|     public function testVoteAction( | ||||
|         $expectedResult, | ||||
|         User $user, | ||||
|         Scope $scope, | ||||
|         Center $center, | ||||
|         $attribute, | ||||
|         $message, | ||||
|     ) { | ||||
|         $token = $this->prepareToken($user); | ||||
|         $activity = $this->prepareActivity($scope, $this->preparePerson($center)); | ||||
|  | ||||
|         $this->assertEquals( | ||||
|             $expectedResult, | ||||
|             $this->voter->vote($token, $activity, [$attribute]), | ||||
|             $message | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function dataProvider_testVoteAction() | ||||
|     { | ||||
|         $centerA = $this->prepareCenter(1, 'center A'); | ||||
| @@ -150,6 +110,46 @@ final class ActivityVoterTest extends KernelTestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testNullUser() | ||||
|     { | ||||
|         $token = $this->prepareToken(); | ||||
|         $center = $this->prepareCenter(1, 'center'); | ||||
|         $person = $this->preparePerson($center); | ||||
|         $scope = $this->prepareScope(1, 'default'); | ||||
|         $activity = $this->prepareActivity($scope, $person); | ||||
|  | ||||
|         $this->assertEquals( | ||||
|             VoterInterface::ACCESS_DENIED, | ||||
|             $this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']), | ||||
|             'assert that a null user is not allowed to see' | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider dataProvider_testVoteAction | ||||
|      * | ||||
|      * @param type   $expectedResult | ||||
|      * @param string $attribute | ||||
|      * @param string $message | ||||
|      */ | ||||
|     public function testVoteAction( | ||||
|         $expectedResult, | ||||
|         User $user, | ||||
|         Scope $scope, | ||||
|         Center $center, | ||||
|         $attribute, | ||||
|         $message, | ||||
|     ) { | ||||
|         $token = $this->prepareToken($user); | ||||
|         $activity = $this->prepareActivity($scope, $this->preparePerson($center)); | ||||
|  | ||||
|         $this->assertEquals( | ||||
|             $expectedResult, | ||||
|             $this->voter->vote($token, $activity, [$attribute]), | ||||
|             $message | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * prepare a token interface with correct rights. | ||||
|      * | ||||
|   | ||||
| @@ -25,7 +25,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte | ||||
|         $config = $this->processConfiguration($configuration, $configs); | ||||
|  | ||||
|         $container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']); | ||||
|         $container->setParameter('chill_aside_activity.show_concerned_persons_count', 'visible' === $config['show_concerned_persons_count']); | ||||
|  | ||||
|         $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); | ||||
|         $loader->load('services.yaml'); | ||||
| @@ -39,24 +38,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte | ||||
|     { | ||||
|         $this->prependRoute($container); | ||||
|         $this->prependCruds($container); | ||||
|         $this->prependTwigConfig($container); | ||||
|     } | ||||
|  | ||||
|     protected function prependTwigConfig(ContainerBuilder $container) | ||||
|     { | ||||
|         // Get the configuration for this bundle | ||||
|         $chillAsideActivityConfig = $container->getExtensionConfig($this->getAlias()); | ||||
|         $config = $this->processConfiguration($this->getConfiguration($chillAsideActivityConfig, $container), $chillAsideActivityConfig); | ||||
|  | ||||
|         // Add configuration to twig globals | ||||
|         $twigConfig = [ | ||||
|             'globals' => [ | ||||
|                 'chill_aside_activity_config' => [ | ||||
|                     'show_concerned_persons_count' => 'visible' === $config['show_concerned_persons_count'], | ||||
|                 ], | ||||
|             ], | ||||
|         ]; | ||||
|         $container->prependExtensionConfig('twig', $twigConfig); | ||||
|     } | ||||
|  | ||||
|     protected function prependCruds(ContainerBuilder $container) | ||||
|   | ||||
| @@ -141,12 +141,6 @@ class Configuration implements ConfigurationInterface | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->enumNode('show_concerned_persons_count') | ||||
|             ->values(['hidden', 'visible']) | ||||
|             ->defaultValue('hidden') | ||||
|             ->info('Show the concerned persons count field in aside activity forms and views') | ||||
|             ->end() | ||||
|             ->end(); | ||||
|  | ||||
|         return $treeBuilder; | ||||
|   | ||||
| @@ -62,10 +62,6 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|     #[ORM\ManyToOne(targetEntity: User::class)] | ||||
|     private User $updatedBy; | ||||
|  | ||||
|     #[Assert\GreaterThanOrEqual(0)] | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)] | ||||
|     private ?int $concernedPersonsCount = 0; | ||||
|  | ||||
|     public function getAgent(): ?User | ||||
|     { | ||||
|         return $this->agent; | ||||
| @@ -190,16 +186,4 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getConcernedPersonsCount(): ?int | ||||
|     { | ||||
|         return $this->concernedPersonsCount; | ||||
|     } | ||||
|  | ||||
|     public function setConcernedPersonsCount(?int $concernedPersonsCount): self | ||||
|     { | ||||
|         $this->concernedPersonsCount = $concernedPersonsCount; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,86 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Export\Aggregator; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Export\AggregatorInterface; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class ByConcernedPersonsCountAggregator implements AggregatorInterface | ||||
| { | ||||
|     public function addRole(): ?string | ||||
|     { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void | ||||
|     { | ||||
|         $qb->addSelect('aside.concernedPersonsCount AS by_concerned_persons_count_aggregator') | ||||
|             ->addGroupBy('by_concerned_persons_count_aggregator'); | ||||
|     } | ||||
|  | ||||
|     public function applyOn(): string | ||||
|     { | ||||
|         return Declarations::ASIDE_ACTIVITY_TYPE; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder): void | ||||
|     { | ||||
|         // No form needed | ||||
|     } | ||||
|  | ||||
|     public function getNormalizationVersion(): int | ||||
|     { | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     public function normalizeFormData(array $formData): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function denormalizeFormData(array $formData, int $fromVersion): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data): callable | ||||
|     { | ||||
|         return function ($value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'export.aggregator.Concerned persons count'; | ||||
|             } | ||||
|  | ||||
|             if (null === $value) { | ||||
|                 return 'export.aggregator.No concerned persons count specified'; | ||||
|             } | ||||
|  | ||||
|             return (string) $value; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data): array | ||||
|     { | ||||
|         return ['by_concerned_persons_count_aggregator']; | ||||
|     } | ||||
|  | ||||
|     public function getTitle(): string | ||||
|     { | ||||
|         return 'export.aggregator.Group by concerned persons count'; | ||||
|     } | ||||
| } | ||||
| @@ -1,116 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Export\Export; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\AsideActivityBundle\Repository\AsideActivityRepository; | ||||
| use Chill\AsideActivityBundle\Security\AsideActivityVoter; | ||||
| use Chill\MainBundle\Export\ExportInterface; | ||||
| use Chill\MainBundle\Export\FormatterInterface; | ||||
| use Chill\MainBundle\Export\GroupedExportInterface; | ||||
| use Doctrine\ORM\Query; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class SumConcernedPersonsCountAsideActivity implements ExportInterface, GroupedExportInterface | ||||
| { | ||||
|     public function __construct(private readonly AsideActivityRepository $repository) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) {} | ||||
|  | ||||
|     public function getNormalizationVersion(): int | ||||
|     { | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     public function normalizeFormData(array $formData): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function denormalizeFormData(array $formData, int $fromVersion): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getAllowedFormattersTypes(): array | ||||
|     { | ||||
|         return [FormatterInterface::TYPE_TABULAR]; | ||||
|     } | ||||
|  | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'export.Sum concerned persons count for aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getGroup(): string | ||||
|     { | ||||
|         return 'export.Exports of aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|     { | ||||
|         if ('export_sum_concerned_persons_count' !== $key) { | ||||
|             throw new \LogicException("the key {$key} is not used by this export"); | ||||
|         } | ||||
|  | ||||
|         $labels = array_combine($values, $values); | ||||
|         $labels['_header'] = $this->getTitle(); | ||||
|  | ||||
|         return static fn ($value) => $labels[$value]; | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data): array | ||||
|     { | ||||
|         return ['export_sum_concerned_persons_count']; | ||||
|     } | ||||
|  | ||||
|     public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array | ||||
|     { | ||||
|         return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); | ||||
|     } | ||||
|  | ||||
|     public function getTitle(): string | ||||
|     { | ||||
|         return 'export.Sum concerned persons count for aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getType(): string | ||||
|     { | ||||
|         return Declarations::ASIDE_ACTIVITY_TYPE; | ||||
|     } | ||||
|  | ||||
|     public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder | ||||
|     { | ||||
|         $qb = $this->repository->createQueryBuilder('aside'); | ||||
|  | ||||
|         $qb->select('SUM(COALESCE(aside.concernedPersonsCount, 0)) as export_sum_concerned_persons_count'); | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function requiredRole(): string | ||||
|     { | ||||
|         return AsideActivityVoter::STATS; | ||||
|     } | ||||
|  | ||||
|     public function supportsModifiers(): array | ||||
|     { | ||||
|         return [ | ||||
|             Declarations::ASIDE_ACTIVITY_TYPE, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\IntegerType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Form\FormEvent; | ||||
| use Symfony\Component\Form\FormEvents; | ||||
| @@ -30,13 +29,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| final class AsideActivityFormType extends AbstractType | ||||
| { | ||||
|     private readonly array $timeChoices; | ||||
|     private readonly bool $showConcernedPersonsCount; | ||||
|  | ||||
|     public function __construct( | ||||
|         ParameterBagInterface $parameterBag, | ||||
|     ) { | ||||
|         $this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration'); | ||||
|         $this->showConcernedPersonsCount = $parameterBag->get('chill_aside_activity.show_concerned_persons_count'); | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
| @@ -79,16 +76,6 @@ final class AsideActivityFormType extends AbstractType | ||||
|             ->add('location', PickUserLocationType::class) | ||||
|         ; | ||||
|  | ||||
|         if ($this->showConcernedPersonsCount) { | ||||
|             $builder->add('concernedPersonsCount', IntegerType::class, [ | ||||
|                 'label' => 'Concerned persons count', | ||||
|                 'required' => false, | ||||
|                 'attr' => [ | ||||
|                     'min' => 0, | ||||
|                 ], | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         foreach (['duration'] as $fieldName) { | ||||
|             $builder->get($fieldName) | ||||
|                 ->addModelTransformer($durationTimeTransformer); | ||||
|   | ||||
| @@ -42,11 +42,6 @@ | ||||
|                                 {%- if entity.location.name is defined -%} | ||||
|                                     <div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div> | ||||
|                                 {%- endif -%} | ||||
|  | ||||
|                                 {%- if entity.concernedPersonsCount > 0 -%} | ||||
|                                     <div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div> | ||||
|                                 {%- endif -%} | ||||
|  | ||||
| 							</div> | ||||
| 							<div class="item-col" style="justify-content: flex-end;"> | ||||
| 								<div class="box"> | ||||
|   | ||||
| @@ -38,11 +38,6 @@ | ||||
| 				<dt class="inline">{{ 'Duration'|trans }}</dt> | ||||
| 				<dd>{{ entity.duration|date('H:i') }}</dd> | ||||
|  | ||||
|                 {% if chill_aside_activity_config.show_concerned_persons_count == 'visible' %} | ||||
|                     <dt class="inline">{{ 'Concerned persons count'|trans }}</dt> | ||||
|                     <dd>{{ entity.concernedPersonsCount }}</dd> | ||||
|                 {% endif %} | ||||
|  | ||||
| 				<dt class="inline">{{ 'Remark'|trans }}</dt> | ||||
| 				{%- if entity.note is empty -%} | ||||
| 					<dd> | ||||
| @@ -60,6 +55,5 @@ | ||||
| 			</dl> | ||||
|  | ||||
| 		{% endblock %} | ||||
|         {% block content_view_actions_duplicate_link %}{% endblock %} | ||||
| 	{% endembed %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -30,18 +30,6 @@ final class AsideActivityControllerTest extends WebTestCase | ||||
|         self::ensureKernelShutdown(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateAsideActivityId | ||||
|      */ | ||||
|     public function testEditWithoutUsers(int $asideActivityId) | ||||
|     { | ||||
|         self::ensureKernelShutdown(); | ||||
|         $client = $this->getClientAuthenticated(); | ||||
|         $client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit"); | ||||
|  | ||||
|         $this->assertEquals(200, $client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     public static function generateAsideActivityId(): iterable | ||||
|     { | ||||
|         self::bootKernel(); | ||||
| @@ -70,6 +58,18 @@ final class AsideActivityControllerTest extends WebTestCase | ||||
|         self::ensureKernelShutdown(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateAsideActivityId | ||||
|      */ | ||||
|     public function testEditWithoutUsers(int $asideActivityId) | ||||
|     { | ||||
|         self::ensureKernelShutdown(); | ||||
|         $client = $this->getClientAuthenticated(); | ||||
|         $client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit"); | ||||
|  | ||||
|         $this->assertEquals(200, $client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     public function testIndexWithoutUsers() | ||||
|     { | ||||
|         self::ensureKernelShutdown(); | ||||
|   | ||||
| @@ -1,49 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Tests\Export\Aggregator; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Entity\AsideActivity; | ||||
| use Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator; | ||||
| use Chill\MainBundle\Test\Export\AbstractAggregatorTest; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ByConcernedPersonsCountAggregatorTest extends AbstractAggregatorTest | ||||
| { | ||||
|     public function getAggregator() | ||||
|     { | ||||
|         return new ByConcernedPersonsCountAggregator(); | ||||
|     } | ||||
|  | ||||
|     public static function getFormData(): array | ||||
|     { | ||||
|         return [ | ||||
|             [], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function getQueryBuilders(): iterable | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $em = self::getContainer()->get(EntityManagerInterface::class); | ||||
|  | ||||
|         return [ | ||||
|             $em->createQueryBuilder() | ||||
|                 ->select('count(aside.id)') | ||||
|                 ->from(AsideActivity::class, 'aside'), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Tests\Export\Export; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity; | ||||
| use Chill\AsideActivityBundle\Repository\AsideActivityRepository; | ||||
| use Chill\MainBundle\Test\Export\AbstractExportTest; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| final class SumConcernedPersonsCountAsideActivityTest extends AbstractExportTest | ||||
| { | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|     } | ||||
|  | ||||
|     public function getExport() | ||||
|     { | ||||
|         $repository = self::getContainer()->get(AsideActivityRepository::class); | ||||
|  | ||||
|         yield new SumConcernedPersonsCountAsideActivity($repository); | ||||
|     } | ||||
|  | ||||
|     public static function getFormData(): array | ||||
|     { | ||||
|         return [ | ||||
|             [], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function getModifiersCombination(): array | ||||
|     { | ||||
|         return [ | ||||
|             ['aside_activity'], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -20,10 +20,6 @@ services: | ||||
|       tags: | ||||
|           - { name: chill.export, alias: 'avg_aside_activity_duration' } | ||||
|  | ||||
|   Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity: | ||||
|       tags: | ||||
|           - { name: chill.export, alias: 'sum_aside_activity_concerned_persons_count' } | ||||
|  | ||||
|   ## Filters | ||||
|   chill.aside_activity.export.date_filter: | ||||
|     class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter | ||||
| @@ -74,7 +70,3 @@ services: | ||||
|   Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator: | ||||
|       tags: | ||||
|           - { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' } | ||||
|  | ||||
|   Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator: | ||||
|       tags: | ||||
|           - { name: chill.export_aggregator, alias: 'aside_activity_concerned_persons_count_aggregator' } | ||||
|   | ||||
| @@ -1,33 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\AsideActivity; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20251006113048 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add concernedPersonsCount property to AsideActivity entity'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_asideactivity.asideactivity ADD concernedPersonsCount INT DEFAULT 0'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_asideactivity.AsideActivity DROP concernedPersonsCount'); | ||||
|     } | ||||
| } | ||||
| @@ -27,7 +27,6 @@ Emergency: Urgent | ||||
| by: "Par " | ||||
| location: Lieu | ||||
| Asideactivity location: Localisation de l'activité | ||||
| Concerned persons count: Nombre d'usager concernés | ||||
|  | ||||
| # Crud | ||||
| crud: | ||||
| @@ -191,7 +190,6 @@ export: | ||||
|     Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères | ||||
|     Average aside activities duration: Durée moyenne des activités annexes | ||||
|     Sum aside activities duration: Durée des activités annexes | ||||
|     Sum concerned persons count for aside activities: Nombre d'usager concernés par les activités annexes | ||||
|     filter: | ||||
|         Filter by aside activity date: Filtrer les activités annexes par date | ||||
|         Filter by aside activity type: Filtrer les activités annexes par type d'activité | ||||
| @@ -212,8 +210,6 @@ export: | ||||
|         'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%" | ||||
|     aggregator: | ||||
|         Group by aside activity type: Grouper les activités annexes par type d'activité | ||||
|         Group by concerned persons count: Grouper les activités annexes par nombre d'usagers conernés | ||||
|         Concerned persons count: Nombre d'usagers concernés | ||||
|         Aside activity type: Type d'activité annexe | ||||
|         by_user_job: | ||||
|             Aggregate by user job: Grouper les activités annexes par métier des utilisateurs | ||||
|   | ||||
| @@ -70,8 +70,6 @@ | ||||
|                         <option value="00:10:00">10 minutes</option> | ||||
|                         <option value="00:15:00">15 minutes</option> | ||||
|                         <option value="00:30:00">30 minutes</option> | ||||
|                         <option value="00:45:00">45 minutes</option> | ||||
|                         <option value="00:60:00">60 minutes</option> | ||||
|                     </select> | ||||
|                     <label class="input-group-text" for="slotMinTime">De</label> | ||||
|                     <select | ||||
|   | ||||
| @@ -32,8 +32,6 @@ | ||||
|                     <option value="00:10:00">10 minutes</option> | ||||
|                     <option value="00:15:00">15 minutes</option> | ||||
|                     <option value="00:30:00">30 minutes</option> | ||||
|                     <option value="00:45:00">45 minutes</option> | ||||
|                     <option value="00:60:00">60 minutes</option> | ||||
|                 </select> | ||||
|                 <label class="input-group-text" for="slotMinTime">De</label> | ||||
|                 <select | ||||
| @@ -104,8 +102,7 @@ | ||||
|                     event.title | ||||
|                 }}</b> | ||||
|                 <b v-else-if="event.extendedProps.is === 'range'" | ||||
|                     >{{ formatDate(event.startStr, "time") }} - | ||||
|                     {{ formatDate(event.endStr, "time") }}: | ||||
|                     >{{ formatDate(event.startStr) }} - | ||||
|                     {{ event.extendedProps.locationName }}</b | ||||
|                 > | ||||
|                 <b v-else-if="event.extendedProps.is === 'local'">{{ | ||||
| @@ -297,26 +294,9 @@ const nextWeeks = computed((): Weeks[] => | ||||
|     }), | ||||
| ); | ||||
|  | ||||
| const formatDate = (datetime: string, format: null | "time" = null) => { | ||||
|     const date = ISOToDate(datetime); | ||||
|     if (!date) return ""; | ||||
|  | ||||
|     if (format === "time") { | ||||
|         return date.toLocaleTimeString("fr-FR", { | ||||
|             hour: "2-digit", | ||||
|             minute: "2-digit", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // French date formatting | ||||
|     return date.toLocaleDateString("fr-FR", { | ||||
|         weekday: "short", | ||||
|         year: "numeric", | ||||
|         month: "short", | ||||
|         day: "numeric", | ||||
|         hour: "2-digit", | ||||
|         minute: "2-digit", | ||||
|     }); | ||||
| const formatDate = (datetime: string) => { | ||||
|     console.log(typeof datetime); | ||||
|     return ISOToDate(datetime); | ||||
| }; | ||||
|  | ||||
| const baseOptions = ref<CalendarOptions>({ | ||||
|   | ||||
| @@ -24,11 +24,7 @@ use Doctrine\ORM\EntityManagerInterface; | ||||
|  | ||||
| class CalendarForShortMessageProvider | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly CalendarRepository $calendarRepository, | ||||
|         private readonly EntityManagerInterface $em, | ||||
|         private readonly RangeGeneratorInterface $rangeGenerator, | ||||
|     ) {} | ||||
|     public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {} | ||||
|  | ||||
|     /** | ||||
|      * Generate calendars instance. | ||||
|   | ||||
| @@ -42,32 +42,6 @@ final class CalendarControllerTest extends WebTestCase | ||||
|         self::ensureKernelShutdown(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testList(int $accompanyingPeriodId) | ||||
|     { | ||||
|         $this->client->request( | ||||
|             Request::METHOD_GET, | ||||
|             sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId) | ||||
|         ); | ||||
|  | ||||
|         $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testNew(int $accompanyingPeriodId) | ||||
|     { | ||||
|         $this->client->request( | ||||
|             Request::METHOD_GET, | ||||
|             sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId) | ||||
|         ); | ||||
|  | ||||
|         $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     public static function provideAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         self::bootKernel(); | ||||
| @@ -108,4 +82,30 @@ final class CalendarControllerTest extends WebTestCase | ||||
|  | ||||
|         self::ensureKernelShutdown(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testList(int $accompanyingPeriodId) | ||||
|     { | ||||
|         $this->client->request( | ||||
|             Request::METHOD_GET, | ||||
|             sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId) | ||||
|         ); | ||||
|  | ||||
|         $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testNew(int $accompanyingPeriodId) | ||||
|     { | ||||
|         $this->client->request( | ||||
|             Request::METHOD_GET, | ||||
|             sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId) | ||||
|         ); | ||||
|  | ||||
|         $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -45,6 +45,20 @@ class MSUserAbsenceReaderTest extends TestCase | ||||
|         self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message); | ||||
|     } | ||||
|  | ||||
|     public function testIsUserAbsentWithoutRemoteId(): void | ||||
|     { | ||||
|         $user = new User(); | ||||
|         $client = new MockHttpClient(); | ||||
|  | ||||
|         $mapUser = $this->prophesize(MapCalendarToUser::class); | ||||
|         $mapUser->getUserId($user)->willReturn(null); | ||||
|         $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); | ||||
|  | ||||
|         $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); | ||||
|  | ||||
|         self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null'); | ||||
|     } | ||||
|  | ||||
|     public static function provideDataTestUserAbsence(): iterable | ||||
|     { | ||||
|         // contains data that was retrieved from microsoft graph api on 2023-07-06 | ||||
| @@ -159,18 +173,4 @@ class MSUserAbsenceReaderTest extends TestCase | ||||
|             'User is absent: absence is always enabled', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testIsUserAbsentWithoutRemoteId(): void | ||||
|     { | ||||
|         $user = new User(); | ||||
|         $client = new MockHttpClient(); | ||||
|  | ||||
|         $mapUser = $this->prophesize(MapCalendarToUser::class); | ||||
|         $mapUser->getUserId($user)->willReturn(null); | ||||
|         $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); | ||||
|  | ||||
|         $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); | ||||
|  | ||||
|         self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification; | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Chill\CalendarBundle\Repository\CalendarRepository; | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider; | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator; | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| @@ -81,16 +82,10 @@ final class CalendarForShortMessageProviderTest extends TestCase | ||||
|         $em = $this->prophesize(EntityManagerInterface::class); | ||||
|         $em->clear()->shouldBeCalled(); | ||||
|  | ||||
|         $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class); | ||||
|         $calendarRangeGenerator->generateRange(Argument::any())->willReturn([ | ||||
|             'startDate' => new \DateTimeImmutable('yesterday'), | ||||
|             'endDate' => new \DateTimeImmutable('now'), | ||||
|         ]); | ||||
|  | ||||
|         $provider = new CalendarForShortMessageProvider( | ||||
|             $calendarRepository->reveal(), | ||||
|             $em->reveal(), | ||||
|             $calendarRangeGenerator->reveal(), | ||||
|             new DefaultRangeGenerator() | ||||
|         ); | ||||
|  | ||||
|         $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); | ||||
| @@ -108,32 +103,26 @@ final class CalendarForShortMessageProviderTest extends TestCase | ||||
|             Argument::type(\DateTimeImmutable::class), | ||||
|             Argument::type('int'), | ||||
|             Argument::exact(0) | ||||
|         )->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1); | ||||
|         )->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1); | ||||
|         $calendarRepository->findByNotificationAvailable( | ||||
|             Argument::type(\DateTimeImmutable::class), | ||||
|             Argument::type(\DateTimeImmutable::class), | ||||
|             Argument::type('int'), | ||||
|             Argument::exact(10) | ||||
|             Argument::not(0) | ||||
|         )->will(static fn ($args) => [])->shouldBeCalledTimes(1); | ||||
|  | ||||
|         $em = $this->prophesize(EntityManagerInterface::class); | ||||
|         $em->clear()->shouldBeCalled(); | ||||
|  | ||||
|         $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class); | ||||
|         $calendarRangeGenerator->generateRange(Argument::any())->willReturn([ | ||||
|             'startDate' => new \DateTimeImmutable('yesterday'), | ||||
|             'endDate' => new \DateTimeImmutable('now'), | ||||
|         ]); | ||||
|  | ||||
|         $provider = new CalendarForShortMessageProvider( | ||||
|             $calendarRepository->reveal(), | ||||
|             $em->reveal(), | ||||
|             $calendarRangeGenerator->reveal(), | ||||
|             new DefaultRangeGenerator() | ||||
|         ); | ||||
|  | ||||
|         $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); | ||||
|  | ||||
|         $this->assertEquals(10, \count($calendars)); | ||||
|         $this->assertEquals(1, \count($calendars)); | ||||
|         $this->assertContainsOnly(Calendar::class, $calendars); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,24 +28,6 @@ use PHPUnit\Framework\TestCase; | ||||
|  */ | ||||
| final class DefaultRangeGeneratorTest extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * @dataProvider generateData | ||||
|      */ | ||||
|     public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate) | ||||
|     { | ||||
|         $generator = new DefaultRangeGenerator(); | ||||
|  | ||||
|         ['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date); | ||||
|  | ||||
|         if (null === $startDate) { | ||||
|             $this->assertNull($actualStartDate); | ||||
|             $this->assertNull($actualEndDate); | ||||
|         } else { | ||||
|             $this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM)); | ||||
|             $this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * * Lundi => Envoi des rdv du mardi et mercredi. | ||||
|      * * Mardi => Envoi des rdv du jeudi. | ||||
| @@ -97,4 +79,22 @@ final class DefaultRangeGeneratorTest extends TestCase | ||||
|             null, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateData | ||||
|      */ | ||||
|     public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate) | ||||
|     { | ||||
|         $generator = new DefaultRangeGenerator(); | ||||
|  | ||||
|         ['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date); | ||||
|  | ||||
|         if (null === $startDate) { | ||||
|             $this->assertNull($actualStartDate); | ||||
|             $this->assertNull($actualEndDate); | ||||
|         } else { | ||||
|             $this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM)); | ||||
|             $this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\CustomFieldsBundle\EntityRepository; | ||||
|  | ||||
| use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup; | ||||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
|  | ||||
| class CustomFieldsDefaultGroupRepository extends ServiceEntityRepository | ||||
| { | ||||
|     public function __construct(ManagerRegistry $registry) | ||||
|     { | ||||
|         parent::__construct($registry, CustomFieldsDefaultGroup::class); | ||||
|     } | ||||
|  | ||||
|     public function findOneByEntity(string $className): ?CustomFieldsDefaultGroup | ||||
|     { | ||||
|         return $this->findOneBy(['entity' => $className]); | ||||
|     } | ||||
| } | ||||
| @@ -49,6 +49,80 @@ final class CustomFieldsChoiceTest extends KernelTestCase | ||||
|         parent::tearDown(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * provide empty data in different possible representations. | ||||
|      * Those data are supposed to be deserialized. | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     public static function emptyDataProvider() | ||||
|     { | ||||
|         return [ | ||||
|             // 0 | ||||
|             [ | ||||
|                 // signle | ||||
|                 '', | ||||
|             ], | ||||
|             // 1 | ||||
|             [ | ||||
|                 // single | ||||
|                 null, | ||||
|             ], | ||||
|             // 2 | ||||
|             [ | ||||
|                 // signle with allow other | ||||
|                 ['_other' => 'something', '_choices' => ''], | ||||
|             ], | ||||
|             // 3 | ||||
|             [ | ||||
|                 // multiple | ||||
|                 [], | ||||
|             ], | ||||
|             // 4 | ||||
|             [ | ||||
|                 // multiple with allow other | ||||
|                 ['_other' => 'something', '_choices' => []], | ||||
|             ], | ||||
|             // 5 | ||||
|             [ | ||||
|                 // multiple with allow other | ||||
|                 ['_other' => '', '_choices' => []], | ||||
|             ], | ||||
|             // 6 | ||||
|             [ | ||||
|                 // empty | ||||
|                 ['_other' => null, '_choices' => null], | ||||
|             ], | ||||
|             // 7 | ||||
|             [ | ||||
|                 // empty | ||||
|                 [null], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function serializedRepresentationDataProvider() | ||||
|     { | ||||
|         return [ | ||||
|             [ | ||||
|                 // multiple => false, allow_other => false | ||||
|                 'my-value', | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => true, allow_ther => false | ||||
|                 ['my-value'], | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => false, allow_other => true, current value not in other | ||||
|                 ['_other' => '', '_choices' => 'my-value'], | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => true, allow_other => true, current value not in other | ||||
|                 ['_other' => '', '_choices' => ['my-value']], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Test if the representation of the data is deserialized to an array text | ||||
|      * with an "allow_other" field. | ||||
| @@ -338,58 +412,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase | ||||
|         $this->assertTrue($isEmpty); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * provide empty data in different possible representations. | ||||
|      * Those data are supposed to be deserialized. | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     public static function emptyDataProvider() | ||||
|     { | ||||
|         return [ | ||||
|             // 0 | ||||
|             [ | ||||
|                 // signle | ||||
|                 '', | ||||
|             ], | ||||
|             // 1 | ||||
|             [ | ||||
|                 // single | ||||
|                 null, | ||||
|             ], | ||||
|             // 2 | ||||
|             [ | ||||
|                 // signle with allow other | ||||
|                 ['_other' => 'something', '_choices' => ''], | ||||
|             ], | ||||
|             // 3 | ||||
|             [ | ||||
|                 // multiple | ||||
|                 [], | ||||
|             ], | ||||
|             // 4 | ||||
|             [ | ||||
|                 // multiple with allow other | ||||
|                 ['_other' => 'something', '_choices' => []], | ||||
|             ], | ||||
|             // 5 | ||||
|             [ | ||||
|                 // multiple with allow other | ||||
|                 ['_other' => '', '_choices' => []], | ||||
|             ], | ||||
|             // 6 | ||||
|             [ | ||||
|                 // empty | ||||
|                 ['_other' => null, '_choices' => null], | ||||
|             ], | ||||
|             // 7 | ||||
|             [ | ||||
|                 // empty | ||||
|                 [null], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     // /////////////////////////////////////// | ||||
|     // | ||||
|     // test function isEmptyValue | ||||
| @@ -413,28 +435,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase | ||||
|         $this->assertFalse($isEmpty); | ||||
|     } | ||||
|  | ||||
|     public static function serializedRepresentationDataProvider() | ||||
|     { | ||||
|         return [ | ||||
|             [ | ||||
|                 // multiple => false, allow_other => false | ||||
|                 'my-value', | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => true, allow_ther => false | ||||
|                 ['my-value'], | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => false, allow_other => true, current value not in other | ||||
|                 ['_other' => '', '_choices' => 'my-value'], | ||||
|             ], | ||||
|             [ | ||||
|                 // multiple => true, allow_other => true, current value not in other | ||||
|                 ['_other' => '', '_choices' => ['my-value']], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array $options | ||||
|      * | ||||
|   | ||||
| @@ -127,7 +127,3 @@ services: | ||||
|         factory: ["@doctrine", getRepository] | ||||
|         arguments: | ||||
|             - "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option" | ||||
|  | ||||
|     Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|   | ||||
| @@ -58,7 +58,6 @@ | ||||
|  | ||||
| <script> | ||||
| import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator"; | ||||
| import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; | ||||
|  | ||||
| export default { | ||||
|     name: "PickTemplate", | ||||
| @@ -114,9 +113,6 @@ export default { | ||||
|         }, | ||||
|     }, | ||||
|     methods: { | ||||
|         localizeString(str) { | ||||
|             return localizeString(str); | ||||
|         }, | ||||
|         clickGenerate(event, link) { | ||||
|             if (!this.preventDefaultMoveToGenerate) { | ||||
|                 window.location.assign(link); | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| {{ 'docgen.data_dump_email.Dear'|trans }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.data_dump_ready_and_attached'|trans }} | ||||
| {{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.filename'|trans({filename: filename}) }} | ||||
| {{ link }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }} | ||||
|   | ||||
| @@ -11,13 +11,13 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\Generator; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; | ||||
| use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| @@ -37,15 +37,15 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|     private const LOG_PREFIX = '[docgen message handler] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository, | ||||
|         private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly GeneratorInterface $generator, | ||||
|         private readonly Generator $generator, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly StoredObjectRepositoryInterface $storedObjectRepository, | ||||
|         private readonly StoredObjectRepository $storedObjectRepository, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|         private readonly MailerInterface $mailer, | ||||
|         private readonly TempUrlGeneratorInterface $tempUrlGenerator, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|         private readonly StoredObjectManagerInterface $storedObjectManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(RequestGenerationMessage $message) | ||||
| @@ -90,7 +90,7 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|  | ||||
|                 $this->sendDataDump($destinationStoredObject, $message); | ||||
|             } else { | ||||
|                 $this->generator->generateDocFromTemplate( | ||||
|                 $destinationStoredObject = $this->generator->generateDocFromTemplate( | ||||
|                     $template, | ||||
|                     $message->getEntityId(), | ||||
|                     $message->getContextGenerationData(), | ||||
| @@ -122,20 +122,19 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|  | ||||
|     private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void | ||||
|     { | ||||
|         // Get the content of the document | ||||
|         $content = $this->storedObjectManager->read($destinationStoredObject); | ||||
|         $filename = $destinationStoredObject->getFilename(); | ||||
|         $contentType = $destinationStoredObject->getType(); | ||||
|         $url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600); | ||||
|         $parts = []; | ||||
|         parse_str(parse_url($url->url)['query'], $parts); | ||||
|         $validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']); | ||||
|  | ||||
|         // Create the email with the document as an attachment | ||||
|         $email = (new TemplatedEmail()) | ||||
|             ->to($message->getSendResultToEmail()) | ||||
|             ->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig') | ||||
|             ->context([ | ||||
|                 'filename' => $filename, | ||||
|                 'link' => $url->url, | ||||
|                 'validity' => $validity, | ||||
|             ]) | ||||
|             ->subject($this->translator->trans('docgen.data_dump_email.subject')) | ||||
|             ->attach($content, $filename, $contentType); | ||||
|             ->subject($this->translator->trans('docgen.data_dump_email.subject')); | ||||
|  | ||||
|         $this->mailer->send($email); | ||||
|     } | ||||
|   | ||||
| @@ -1,132 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Tests\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationHandler; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\Query; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class RequestGenerationHandlerTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testGenerationHappyScenario(): void | ||||
|     { | ||||
|         // Create entities | ||||
|         $template = new DocGeneratorTemplate(); | ||||
|         $this->setPrivateProperty($template, 'id', 1); | ||||
|  | ||||
|         $storedObject = new StoredObject(); | ||||
|         $this->setPrivateProperty($storedObject, 'id', 2); | ||||
|  | ||||
|         $creator = new User(); | ||||
|         $creator->setEmail('test@example.com'); | ||||
|         $this->setPrivateProperty($creator, 'id', 3); | ||||
|  | ||||
|         $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); | ||||
|         $docGeneratorTemplateRepository->find(1)->willReturn($template); | ||||
|  | ||||
|         $storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class); | ||||
|         $storedObjectRepository->find(2)->willReturn($storedObject); | ||||
|  | ||||
|         $userRepository = $this->prophesize(UserRepositoryInterface::class); | ||||
|         $userRepository->find(3)->willReturn($creator); | ||||
|  | ||||
|         // Create a mock for the Query object | ||||
|         $query = $this->prophesize(Query::class); | ||||
|         $query->setParameter('id', 2)->willReturn($query->reveal()); | ||||
|         $query->execute()->shouldBeCalled(); | ||||
|  | ||||
|         // Create a mock for the EntityManager | ||||
|         $entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $entityManager->createQuery(Argument::containingString('UPDATE'))->willReturn($query->reveal()); | ||||
|         $entityManager->flush()->shouldBeCalled(); | ||||
|  | ||||
|         $generator = $this->prophesize(GeneratorInterface::class); | ||||
|         $generator->generateDocFromTemplate( | ||||
|             $template, | ||||
|             123, // entityId | ||||
|             ['key' => 'value'], // contextGenerationData | ||||
|             $storedObject, | ||||
|             $creator | ||||
|         ) | ||||
|             ->willReturn($storedObject)->shouldBeCalled(); | ||||
|  | ||||
|         $logger = new NullLogger(); | ||||
|  | ||||
|         $mailer = $this->prophesize(MailerInterface::class); | ||||
|  | ||||
|         $translator = $this->prophesize(TranslatorInterface::class); | ||||
|  | ||||
|         $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|  | ||||
|         // Create handler | ||||
|         $handler = new RequestGenerationHandler( | ||||
|             $docGeneratorTemplateRepository->reveal(), | ||||
|             $entityManager->reveal(), | ||||
|             $generator->reveal(), | ||||
|             $logger, | ||||
|             $storedObjectRepository->reveal(), | ||||
|             $userRepository->reveal(), | ||||
|             $mailer->reveal(), | ||||
|             $translator->reveal(), | ||||
|             $storedObjectManager->reveal() | ||||
|         ); | ||||
|  | ||||
|         // Create message | ||||
|         $message = new RequestGenerationMessage( | ||||
|             $creator, | ||||
|             $template, | ||||
|             123, // entityId | ||||
|             $storedObject, | ||||
|             ['key' => 'value'], // contextGenerationData | ||||
|             false, // isTest | ||||
|             null, // sendResultToEmail | ||||
|             false // dumpOnly | ||||
|         ); | ||||
|  | ||||
|         // Invoke handler | ||||
|         $handler->__invoke($message); | ||||
|  | ||||
|         // Assertions | ||||
|         // The assertions are handled by the shouldBeCalled() expectations on the mocks | ||||
|         $this->assertTrue(true); // Just to have an assertion in the test | ||||
|     } | ||||
|  | ||||
|     private function setPrivateProperty(object $object, string $propertyName, $value): void | ||||
|     { | ||||
|         $reflection = new \ReflectionClass($object); | ||||
|         $property = $reflection->getProperty($propertyName); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($object, $value); | ||||
|     } | ||||
| } | ||||
| @@ -31,36 +31,6 @@ final class DocGenEncoderTest extends TestCase | ||||
|         $this->encoder = new DocGenEncoder(); | ||||
|     } | ||||
|  | ||||
|     public function testEmbeddedLoopsThrowsException() | ||||
|     { | ||||
|         $this->expectException(UnexpectedValueException::class); | ||||
|  | ||||
|         $data = [ | ||||
|             'data' => [ | ||||
|                 ['item' => 'one'], | ||||
|                 [ | ||||
|                     'embedded' => [ | ||||
|                         [ | ||||
|                             ['subitem' => 'two'], | ||||
|                             ['subitem' => 'three'], | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|             ], | ||||
|         ]; | ||||
|  | ||||
|         $this->encoder->encode($data, 'docgen'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateEncodeData | ||||
|      */ | ||||
|     public function testEncode(mixed $expected, mixed $data, string $msg) | ||||
|     { | ||||
|         $generated = $this->encoder->encode($data, 'docgen'); | ||||
|         $this->assertEquals($expected, $generated, $msg); | ||||
|     } | ||||
|  | ||||
|     public static function generateEncodeData() | ||||
|     { | ||||
|         yield [['tests' => 'ok'], ['tests' => 'ok'], 'A simple test with a simple array']; | ||||
| @@ -123,4 +93,34 @@ final class DocGenEncoderTest extends TestCase | ||||
|             'a longer list, with near real data inside and embedded associative arrays', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testEmbeddedLoopsThrowsException() | ||||
|     { | ||||
|         $this->expectException(UnexpectedValueException::class); | ||||
|  | ||||
|         $data = [ | ||||
|             'data' => [ | ||||
|                 ['item' => 'one'], | ||||
|                 [ | ||||
|                     'embedded' => [ | ||||
|                         [ | ||||
|                             ['subitem' => 'two'], | ||||
|                             ['subitem' => 'three'], | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|             ], | ||||
|         ]; | ||||
|  | ||||
|         $this->encoder->encode($data, 'docgen'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateEncodeData | ||||
|      */ | ||||
|     public function testEncode(mixed $expected, mixed $data, string $msg) | ||||
|     { | ||||
|         $generated = $this->encoder->encode($data, 'docgen'); | ||||
|         $this->assertEquals($expected, $generated, $msg); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,2 +1,4 @@ | ||||
| docgen: | ||||
|     # No ICU messages needed for data_dump_email anymore | ||||
|     data_dump_email: | ||||
|         link_valid_until: >- | ||||
|             Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium} | ||||
|   | ||||
| @@ -34,10 +34,8 @@ docgen: | ||||
|     data_dump_email: | ||||
|         subject: Contenu des données de génération de document disponible | ||||
|         Dear: Cher | ||||
|         data_dump_ready_and_attached: >- | ||||
|             Le contenu des données est disponible. Vous le trouverez en pièce jointe à cet email. | ||||
|         filename: >- | ||||
|             Nom du fichier: %filename% | ||||
|         data_dump_ready_and_link: >- | ||||
|             Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant: | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -18,7 +18,6 @@ use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Filesystem\Exception\IOExceptionInterface; | ||||
| use Symfony\Component\Filesystem\Filesystem; | ||||
| use Symfony\Component\Filesystem\Path; | ||||
|  | ||||
| @@ -148,11 +147,16 @@ class StoredObjectManager implements StoredObjectManagerInterface | ||||
|     public function writeContent(string $filename, string $encryptedContent): void | ||||
|     { | ||||
|         $fullPath = $this->buildPath($filename); | ||||
|         $dir = Path::getDirectory($fullPath); | ||||
|  | ||||
|         try { | ||||
|             $this->filesystem->dumpFile($fullPath, $encryptedContent); | ||||
|         } catch (IOExceptionInterface $exception) { | ||||
|             throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception); | ||||
|         if (!$this->filesystem->exists($dir)) { | ||||
|             $this->filesystem->mkdir($dir); | ||||
|         } | ||||
|  | ||||
|         $result = file_put_contents($fullPath, $encryptedContent); | ||||
|  | ||||
|         if (false === $result) { | ||||
|             throw StoredObjectManagerException::unableToStoreDocumentOnDisk(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController | ||||
|  | ||||
|         return new JsonResponse( | ||||
|             $this->serializer->serialize( | ||||
|                 new Collection(array_values($items->toArray()), $paginator), | ||||
|                 new Collection($items, $paginator), | ||||
|                 'json', | ||||
|                 [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]] | ||||
|             ), | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Exception; | ||||
|  | ||||
| class ConversionWithSameMimeTypeException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(string $mimeType, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous); | ||||
|     } | ||||
| } | ||||
| @@ -25,7 +25,7 @@ export interface GenericDoc { | ||||
|     type: "doc_store_generic_doc"; | ||||
|     uniqueKey: string; | ||||
|     key: string; | ||||
|     identifiers: { id: number }; | ||||
|     identifiers: object; | ||||
|     context: "person" | "accompanying-period"; | ||||
|     doc_date: DateTime; | ||||
|     metadata: GenericDocMetadata; | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { StoredObject, StoredObjectVersion } from "../../types"; | ||||
| import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue"; | ||||
| import { computed, reactive } from "vue"; | ||||
| import { useToast } from "vue-toast-notification"; | ||||
| import { DOCUMENT_ADD, trans } from "translator"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     allowRemove: boolean; | ||||
| @@ -76,9 +75,11 @@ function closeModal(): void { | ||||
|         @click="openModal" | ||||
|         class="btn btn-create" | ||||
|     > | ||||
|         {{ trans(DOCUMENT_ADD) }} | ||||
|         Ajouter un document | ||||
|     </button> | ||||
|     <button v-else @click="openModal" class="btn btn-edit"> | ||||
|         Remplacer le document | ||||
|     </button> | ||||
|     <button v-else @click="openModal" class="btn btn-edit"></button> | ||||
|     <modal | ||||
|         v-if="state.showModal" | ||||
|         :modal-dialog-class="modalClasses" | ||||
|   | ||||
| @@ -3,9 +3,9 @@ import { | ||||
|     StoredObject, | ||||
|     StoredObjectPointInTime, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
| } from "ChillDocStoreAssets/types"; | ||||
| } from "./../../../types"; | ||||
| import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; | ||||
| import { ISOToDatetime } from "ChillMainAssets/chill/js/date"; | ||||
| import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date"; | ||||
| import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue"; | ||||
| import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue"; | ||||
| import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue"; | ||||
|   | ||||
| @@ -46,16 +46,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface | ||||
|  | ||||
|     public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool | ||||
|     { | ||||
|         // we first try to get the permission from the workflow, as attachement (this is the less intensive query) | ||||
|         $workflowPermissionAsAttachment = match ($attribute) { | ||||
|             StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject), | ||||
|             StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject), | ||||
|         }; | ||||
|  | ||||
|         if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Retrieve the related entity | ||||
|         $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); | ||||
|  | ||||
| @@ -76,7 +66,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface | ||||
|         return match ($workflowPermission) { | ||||
|             WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, | ||||
|             WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, | ||||
|             WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission, | ||||
|             WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,12 +14,6 @@ namespace Chill\DocStoreBundle\Security\Authorization; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||
|  | ||||
| /** | ||||
|  * Interface for voting on stored object permissions. | ||||
|  * | ||||
|  * Each time a stored object is attached to a document, the voter is responsible for determining | ||||
|  * whether the user has the necessary permissions to access or modify the stored object. | ||||
|  */ | ||||
| interface StoredObjectVoterInterface | ||||
| { | ||||
|     public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; | ||||
|   | ||||
| @@ -15,7 +15,6 @@ use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectVersion; | ||||
| use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use Symfony\Component\Mime\MimeTypesInterface; | ||||
| @@ -42,10 +41,9 @@ class StoredObjectToPdfConverter | ||||
|      * | ||||
|      * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true | ||||
|      * | ||||
|      * @throws \UnexpectedValueException           if the preferred mime type for the conversion is not found | ||||
|      * @throws \RuntimeException                   if the conversion or storage of the new version fails | ||||
|      * @throws \UnexpectedValueException    if the preferred mime type for the conversion is not found | ||||
|      * @throws \RuntimeException            if the conversion or storage of the new version fails | ||||
|      * @throws StoredObjectManagerException | ||||
|      * @throws ConversionWithSameMimeTypeException if the document has already the same mime type79* | ||||
|      */ | ||||
|     public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array | ||||
|     { | ||||
| @@ -58,7 +56,7 @@ class StoredObjectToPdfConverter | ||||
|         $currentVersion = $storedObject->getCurrentVersion(); | ||||
|  | ||||
|         if ($currentVersion->getType() === $newMimeType) { | ||||
|             throw new ConversionWithSameMimeTypeException($newMimeType); | ||||
|             throw new \UnexpectedValueException('Already at the same mime type'); | ||||
|         } | ||||
|  | ||||
|         $content = $this->storedObjectManager->read($currentVersion); | ||||
|   | ||||
| @@ -85,69 +85,6 @@ class TempUrlLocalStorageGeneratorTest extends TestCase | ||||
|         self::assertEquals($expected, $urlGenerator->validateSignature($signature, $method, $objectName, $expiration), $message); | ||||
|     } | ||||
|  | ||||
|     public static function generateValidateSignatureData(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             true, | ||||
|             'Valid signature, not expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'HEAD', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             true, | ||||
|             'Valid signature, not expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A', | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Invalid signature', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)), | ||||
|             false, | ||||
|             'Signature expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name.'____', | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Invalid object name', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'POST', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Wrong method', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateValidateSignaturePostData | ||||
|      */ | ||||
| @@ -227,6 +164,69 @@ class TempUrlLocalStorageGeneratorTest extends TestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function generateValidateSignatureData(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             true, | ||||
|             'Valid signature, not expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'HEAD', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             true, | ||||
|             'Valid signature, not expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A', | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Invalid signature', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)), | ||||
|             false, | ||||
|             'Signature expired', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'GET', | ||||
|             $object_name.'____', | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Invalid object name', | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180), | ||||
|             'POST', | ||||
|             $object_name, | ||||
|             $expiration, | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)), | ||||
|             false, | ||||
|             'Wrong method', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function buildGenerator(?UrlGeneratorInterface $urlGenerator = null, ?ClockInterface $clock = null): TempUrlLocalStorageGenerator | ||||
|     { | ||||
|         return new TempUrlLocalStorageGenerator( | ||||
|   | ||||
| @@ -31,20 +31,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; | ||||
|  */ | ||||
| final class StoredObjectManagerTest extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * @dataProvider getDataProviderForRead | ||||
|      */ | ||||
|     public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) | ||||
|     { | ||||
|         if (null !== $exceptionClass) { | ||||
|             $this->expectException($exceptionClass); | ||||
|         } | ||||
|  | ||||
|         $storedObjectManager = $this->getSubject($storedObject, $encodedContent); | ||||
|  | ||||
|         self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); | ||||
|     } | ||||
|  | ||||
|     public static function getDataProviderForRead(): \Generator | ||||
|     { | ||||
|         /* HAPPY SCENARIO */ | ||||
| @@ -110,40 +96,6 @@ final class StoredObjectManagerTest extends TestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getDataProviderForWrite | ||||
|      */ | ||||
|     public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null) | ||||
|     { | ||||
|         if (null !== $exceptionClass) { | ||||
|             $this->expectException($exceptionClass); | ||||
|         } | ||||
|  | ||||
|         $previousVersion = $storedObject->getCurrentVersion(); | ||||
|         $previousFilename = $previousVersion->getFilename(); | ||||
|  | ||||
|         $client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) { | ||||
|             self::assertEquals('PUT', $method); | ||||
|             self::assertStringStartsWith('https://example.com/', $url); | ||||
|             self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file'); | ||||
|             self::assertArrayHasKey('body', $options); | ||||
|             self::assertEquals($encodedContent, $options['body']); | ||||
|  | ||||
|             if (-1 === $errorCode) { | ||||
|                 throw new TransportException(); | ||||
|             } | ||||
|  | ||||
|             return new MockResponse('', ['http_code' => $errorCode ?? 201]); | ||||
|         }); | ||||
|  | ||||
|         $storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); | ||||
|  | ||||
|         $newVersion = $storedObjectManager->write($storedObject, $clearContent); | ||||
|  | ||||
|         self::assertNotSame($previousVersion, $newVersion); | ||||
|         self::assertSame($storedObject->getCurrentVersion(), $newVersion); | ||||
|     } | ||||
|  | ||||
|     public static function getDataProviderForWrite(): \Generator | ||||
|     { | ||||
|         /* HAPPY SCENARIO */ | ||||
| @@ -198,6 +150,54 @@ final class StoredObjectManagerTest extends TestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getDataProviderForRead | ||||
|      */ | ||||
|     public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) | ||||
|     { | ||||
|         if (null !== $exceptionClass) { | ||||
|             $this->expectException($exceptionClass); | ||||
|         } | ||||
|  | ||||
|         $storedObjectManager = $this->getSubject($storedObject, $encodedContent); | ||||
|  | ||||
|         self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider getDataProviderForWrite | ||||
|      */ | ||||
|     public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null) | ||||
|     { | ||||
|         if (null !== $exceptionClass) { | ||||
|             $this->expectException($exceptionClass); | ||||
|         } | ||||
|  | ||||
|         $previousVersion = $storedObject->getCurrentVersion(); | ||||
|         $previousFilename = $previousVersion->getFilename(); | ||||
|  | ||||
|         $client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) { | ||||
|             self::assertEquals('PUT', $method); | ||||
|             self::assertStringStartsWith('https://example.com/', $url); | ||||
|             self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file'); | ||||
|             self::assertArrayHasKey('body', $options); | ||||
|             self::assertEquals($encodedContent, $options['body']); | ||||
|  | ||||
|             if (-1 === $errorCode) { | ||||
|                 throw new TransportException(); | ||||
|             } | ||||
|  | ||||
|             return new MockResponse('', ['http_code' => $errorCode ?? 201]); | ||||
|         }); | ||||
|  | ||||
|         $storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); | ||||
|  | ||||
|         $newVersion = $storedObjectManager->write($storedObject, $clearContent); | ||||
|  | ||||
|         self::assertNotSame($previousVersion, $newVersion); | ||||
|         self::assertSame($storedObject->getCurrentVersion(), $newVersion); | ||||
|     } | ||||
|  | ||||
|     public function testDelete(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|   | ||||
| @@ -82,38 +82,6 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase | ||||
|         self::assertEquals($expected, $signedUrl); | ||||
|     } | ||||
|  | ||||
|     public static function dataProviderGenerate(): iterable | ||||
|     { | ||||
|         $now = \DateTimeImmutable::createFromFormat('U', '1702041743'); | ||||
|         $expireDelay = 1800; | ||||
|         $baseUrls = [ | ||||
|             'https://objectstore.example/v1/my_account/container/', | ||||
|             'https://objectstore.example/v1/my_account/container', | ||||
|         ]; | ||||
|         $objectName = 'object'; | ||||
|         $method = 'GET'; | ||||
|         $key = 'MYKEY'; | ||||
|  | ||||
|         $signedUrl = new SignedUrl( | ||||
|             'GET', | ||||
|             'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543', | ||||
|             \DateTimeImmutable::createFromFormat('U', '1702043543'), | ||||
|             $objectName | ||||
|         ); | ||||
|  | ||||
|         foreach ($baseUrls as $baseUrl) { | ||||
|             yield [ | ||||
|                 $baseUrl, | ||||
|                 $now, | ||||
|                 $key, | ||||
|                 $method, | ||||
|                 $objectName, | ||||
|                 $expireDelay, | ||||
|                 $signedUrl, | ||||
|             ]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider dataProviderGeneratePost | ||||
|      */ | ||||
| @@ -157,6 +125,38 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase | ||||
|         self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix)); | ||||
|     } | ||||
|  | ||||
|     public static function dataProviderGenerate(): iterable | ||||
|     { | ||||
|         $now = \DateTimeImmutable::createFromFormat('U', '1702041743'); | ||||
|         $expireDelay = 1800; | ||||
|         $baseUrls = [ | ||||
|             'https://objectstore.example/v1/my_account/container/', | ||||
|             'https://objectstore.example/v1/my_account/container', | ||||
|         ]; | ||||
|         $objectName = 'object'; | ||||
|         $method = 'GET'; | ||||
|         $key = 'MYKEY'; | ||||
|  | ||||
|         $signedUrl = new SignedUrl( | ||||
|             'GET', | ||||
|             'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543', | ||||
|             \DateTimeImmutable::createFromFormat('U', '1702043543'), | ||||
|             $objectName | ||||
|         ); | ||||
|  | ||||
|         foreach ($baseUrls as $baseUrl) { | ||||
|             yield [ | ||||
|                 $baseUrl, | ||||
|                 $now, | ||||
|                 $key, | ||||
|                 $method, | ||||
|                 $objectName, | ||||
|                 $expireDelay, | ||||
|                 $signedUrl, | ||||
|             ]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static function dataProviderGeneratePost(): iterable | ||||
|     { | ||||
|         $now = \DateTimeImmutable::createFromFormat('U', '1702041743'); | ||||
|   | ||||
| @@ -61,55 +61,6 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase | ||||
|         $controller->contentOperate($request); | ||||
|     } | ||||
|  | ||||
|     public static function generateOperateContentWithExceptionDataProvider(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             new Request(['object_name' => '', 'sig' => '', 'exp' => 0]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Object name parameter is missing', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Expiration is not set or equal to zero', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Signature is not set or is a blank string', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             AccessDeniedHttpException::class, | ||||
|             'Invalid signature', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             NotFoundHttpException::class, | ||||
|             'Object does not exists on disk', | ||||
|             false, | ||||
|             '', | ||||
|             true, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testOperateContentGetHappyScenario(): void | ||||
|     { | ||||
|         $objectName = 'testABC'; | ||||
| @@ -335,4 +286,53 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase | ||||
|             'Filename does not start with signed prefix', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function generateOperateContentWithExceptionDataProvider(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             new Request(['object_name' => '', 'sig' => '', 'exp' => 0]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Object name parameter is missing', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Expiration is not set or equal to zero', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             BadRequestHttpException::class, | ||||
|             'Signature is not set or is a blank string', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             AccessDeniedHttpException::class, | ||||
|             'Invalid signature', | ||||
|             false, | ||||
|             '', | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|  | ||||
|         yield [ | ||||
|             new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]), | ||||
|             NotFoundHttpException::class, | ||||
|             'Object does not exists on disk', | ||||
|             false, | ||||
|             '', | ||||
|             true, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,10 +40,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase | ||||
|             $storedObject->registerVersion(); | ||||
|         } | ||||
|  | ||||
|         // remove one version in the history | ||||
|         $v5 = $storedObject->getVersions()->get(5); | ||||
|         $storedObject->removeVersion($v5); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject) | ||||
|             ->willReturn(true) | ||||
| @@ -57,7 +53,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase | ||||
|         self::assertEquals($response->getStatusCode(), 200); | ||||
|         self::assertIsArray($body); | ||||
|         self::assertArrayHasKey('results', $body); | ||||
|         self::assertIsList($body['results']); | ||||
|         self::assertCount(10, $body['results']); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -136,6 +136,63 @@ class WebdavControllerTest extends KernelTestCase | ||||
|         self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateDataPropfindDirectory | ||||
|      */ | ||||
|     public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void | ||||
|     { | ||||
|         $controller = $this->buildController(); | ||||
|  | ||||
|         $request = new Request([], [], [], [], [], [], $requestContent); | ||||
|         $request->setMethod('PROPFIND'); | ||||
|         $request->headers->add(['Depth' => '0']); | ||||
|         $response = $controller->propfindDirectory($this->buildDocument(), '1234', $request); | ||||
|  | ||||
|         self::assertEquals($expectedStatusCode, $response->getStatusCode()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
|         self::assertStringContainsString('text/xml', $response->headers->get('content-type')); | ||||
|         self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml'); | ||||
|         self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); | ||||
|     } | ||||
|  | ||||
|     public function testHeadDocument(): void | ||||
|     { | ||||
|         $controller = $this->buildController(); | ||||
|         $response = $controller->headDocument($this->buildDocument()); | ||||
|  | ||||
|         self::assertEquals(200, $response->getStatusCode()); | ||||
|         self::assertContains('content-length', $response->headers->keys()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
|         self::assertContains('etag', $response->headers->keys()); | ||||
|         self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag')); | ||||
|         self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type')); | ||||
|         self::assertEquals(5, $response->headers->get('content-length')); | ||||
|     } | ||||
|  | ||||
|     public function testPutDocument(): void | ||||
|     { | ||||
|         $document = $this->buildDocument(); | ||||
|         $entityManager = $this->createMock(EntityManagerInterface::class); | ||||
|         $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class); | ||||
|  | ||||
|         // entity manager must be flushed | ||||
|         $entityManager->expects($this->once()) | ||||
|             ->method('flush'); | ||||
|  | ||||
|         // object must be written by StoredObjectManager | ||||
|         $storedObjectManager->expects($this->once()) | ||||
|             ->method('write') | ||||
|             ->with($this->identicalTo($document), $this->identicalTo('1234')); | ||||
|  | ||||
|         $controller = $this->buildController($entityManager, $storedObjectManager); | ||||
|  | ||||
|         $request = new Request(content: '1234'); | ||||
|         $response = $controller->putDocument($document, $request); | ||||
|  | ||||
|         self::assertEquals(204, $response->getStatusCode()); | ||||
|         self::assertEquals('', $response->getContent()); | ||||
|     } | ||||
|  | ||||
|     public static function generateDataPropfindDocument(): iterable | ||||
|     { | ||||
|         $content = | ||||
| @@ -290,25 +347,6 @@ class WebdavControllerTest extends KernelTestCase | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider generateDataPropfindDirectory | ||||
|      */ | ||||
|     public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void | ||||
|     { | ||||
|         $controller = $this->buildController(); | ||||
|  | ||||
|         $request = new Request([], [], [], [], [], [], $requestContent); | ||||
|         $request->setMethod('PROPFIND'); | ||||
|         $request->headers->add(['Depth' => '0']); | ||||
|         $response = $controller->propfindDirectory($this->buildDocument(), '1234', $request); | ||||
|  | ||||
|         self::assertEquals($expectedStatusCode, $response->getStatusCode()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
|         self::assertStringContainsString('text/xml', $response->headers->get('content-type')); | ||||
|         self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml'); | ||||
|         self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); | ||||
|     } | ||||
|  | ||||
|     public static function generateDataPropfindDirectory(): iterable | ||||
|     { | ||||
|         yield [ | ||||
| @@ -376,44 +414,6 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             'test creatableContentsInfo', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testHeadDocument(): void | ||||
|     { | ||||
|         $controller = $this->buildController(); | ||||
|         $response = $controller->headDocument($this->buildDocument()); | ||||
|  | ||||
|         self::assertEquals(200, $response->getStatusCode()); | ||||
|         self::assertContains('content-length', $response->headers->keys()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
|         self::assertContains('etag', $response->headers->keys()); | ||||
|         self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag')); | ||||
|         self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type')); | ||||
|         self::assertEquals(5, $response->headers->get('content-length')); | ||||
|     } | ||||
|  | ||||
|     public function testPutDocument(): void | ||||
|     { | ||||
|         $document = $this->buildDocument(); | ||||
|         $entityManager = $this->createMock(EntityManagerInterface::class); | ||||
|         $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class); | ||||
|  | ||||
|         // entity manager must be flushed | ||||
|         $entityManager->expects($this->once()) | ||||
|             ->method('flush'); | ||||
|  | ||||
|         // object must be written by StoredObjectManager | ||||
|         $storedObjectManager->expects($this->once()) | ||||
|             ->method('write') | ||||
|             ->with($this->identicalTo($document), $this->identicalTo('1234')); | ||||
|  | ||||
|         $controller = $this->buildController($entityManager, $storedObjectManager); | ||||
|  | ||||
|         $request = new Request(content: '1234'); | ||||
|         $response = $controller->putDocument($document, $request); | ||||
|  | ||||
|         self::assertEquals(204, $response->getStatusCode()); | ||||
|         self::assertEquals('', $response->getContent()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| class MockedStoredObjectManager implements StoredObjectManagerInterface | ||||
|   | ||||
| @@ -87,16 +87,6 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase | ||||
|         self::assertIsInt($nb, 'test that the query could be executed'); | ||||
|     } | ||||
|  | ||||
|     public static function provideDataBuildFetchQueryForPerson(): iterable | ||||
|     { | ||||
|         yield [null, null, null]; | ||||
|         yield [new \DateTimeImmutable('1 year ago'), null, null]; | ||||
|         yield [null, new \DateTimeImmutable('1 year ago'), null]; | ||||
|         yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null]; | ||||
|         yield [null, null, 'test']; | ||||
|         yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test']; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDateForFetchQueryForAccompanyingPeriod | ||||
|      */ | ||||
| @@ -152,4 +142,14 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase | ||||
|         yield [$period, null, null, 'test']; | ||||
|         yield [$period, new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test']; | ||||
|     } | ||||
|  | ||||
|     public static function provideDataBuildFetchQueryForPerson(): iterable | ||||
|     { | ||||
|         yield [null, null, null]; | ||||
|         yield [new \DateTimeImmutable('1 year ago'), null, null]; | ||||
|         yield [null, new \DateTimeImmutable('1 year ago'), null]; | ||||
|         yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null]; | ||||
|         yield [null, null, 'test']; | ||||
|         yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test']; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -86,165 +86,9 @@ class AbstractStoredObjectVoterTest extends TestCase | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission | ||||
|      * @dataProvider dataProviderVoteOnAttribute | ||||
|      */ | ||||
|     public function testVoteOnAttributeWithStoredObjectPermission( | ||||
|         StoredObjectRoleEnum $attribute, | ||||
|         bool $expected, | ||||
|         bool $isGrantedRegularPermission, | ||||
|         string $isGrantedWorkflowPermission, | ||||
|         string $isGrantedStoredObjectAttachment, | ||||
|     ): void { | ||||
|         $storedObject = new StoredObject(); | ||||
|         $repository = new DummyRepository($related = new \stdClass()); | ||||
|         $token = new UsernamePasswordToken(new User(), 'dummy'); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); | ||||
|  | ||||
|         $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); | ||||
|  | ||||
|         if (StoredObjectRoleEnum::SEE === $attribute) { | ||||
|             $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject) | ||||
|                 ->shouldBeCalled() | ||||
|                 ->willReturn($isGrantedStoredObjectAttachment); | ||||
|             $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) | ||||
|                 ->willReturn($isGrantedWorkflowPermission); | ||||
|         } elseif (StoredObjectRoleEnum::EDIT === $attribute) { | ||||
|             $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject) | ||||
|                 ->shouldBeCalled() | ||||
|                 ->willReturn($isGrantedStoredObjectAttachment); | ||||
|             $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) | ||||
|                 ->willReturn($isGrantedWorkflowPermission); | ||||
|         } else { | ||||
|             throw new \LogicException('Invalid attribute for StoredObjectVoter'); | ||||
|         } | ||||
|  | ||||
|         $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter { | ||||
|             public function __construct(private $repository, $helper, $security) | ||||
|             { | ||||
|                 parent::__construct($security, $helper); | ||||
|             } | ||||
|  | ||||
|             protected function getRepository(): AssociatedEntityToStoredObjectInterface | ||||
|             { | ||||
|                 return $this->repository; | ||||
|             } | ||||
|  | ||||
|             protected function getClass(): string | ||||
|             { | ||||
|                 return \stdClass::class; | ||||
|             } | ||||
|  | ||||
|             protected function attributeToRole(StoredObjectRoleEnum $attribute): string | ||||
|             { | ||||
|                 return 'SOME_ROLE'; | ||||
|             } | ||||
|  | ||||
|             protected function canBeAssociatedWithWorkflow(): bool | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         $actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token); | ||||
|  | ||||
|         self::assertEquals($expected, $actual); | ||||
|     } | ||||
|  | ||||
|     public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable | ||||
|     { | ||||
|         foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) { | ||||
|             yield 'Not related to any workflow nor attachment ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 true, | ||||
|                 true, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 false, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 true, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 true, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 true, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 true, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 false, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 false, | ||||
|                 false, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 true, | ||||
|                 false, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|             ]; | ||||
|  | ||||
|             yield 'Force grant inverse the regular permission (so) ('.$action.')' => [ | ||||
|                 $attribute, | ||||
|                 true, | ||||
|                 false, | ||||
|                 WorkflowRelatedEntityPermissionHelper::ABSTAIN, | ||||
|                 WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, | ||||
|             ]; | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission | ||||
|      */ | ||||
|     public function testVoteOnAttributeWithoutStoredObjectPermission( | ||||
|     public function testVoteOnAttribute( | ||||
|         StoredObjectRoleEnum $attribute, | ||||
|         bool $expected, | ||||
|         bool $canBeAssociatedWithWorkflow, | ||||
| @@ -261,10 +105,6 @@ class AbstractStoredObjectVoterTest extends TestCase | ||||
|         $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); | ||||
|  | ||||
|         $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); | ||||
|  | ||||
|         $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); | ||||
|         $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); | ||||
|  | ||||
|         if (null !== $isGrantedWorkflowPermissionRead) { | ||||
|             $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) | ||||
|                 ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled(); | ||||
| @@ -283,7 +123,7 @@ class AbstractStoredObjectVoterTest extends TestCase | ||||
|         self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); | ||||
|     } | ||||
|  | ||||
|     public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable | ||||
|     public static function dataProviderVoteOnAttribute(): iterable | ||||
|     { | ||||
|         // not associated on a workflow | ||||
|         yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper']; | ||||
|   | ||||
| @@ -50,6 +50,19 @@ class StoredObjectVoterTest extends TestCase | ||||
|         self::assertEquals($expected, $voter->vote($token, $subject, [$attribute])); | ||||
|     } | ||||
|  | ||||
|     private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface | ||||
|     { | ||||
|         $storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class); | ||||
|         $storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports') | ||||
|             ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class)) | ||||
|             ->willReturn($supports); | ||||
|         $storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute') | ||||
|             ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class)) | ||||
|             ->willReturn($voteOnAttribute); | ||||
|  | ||||
|         return $storedObjectVoter; | ||||
|     } | ||||
|  | ||||
|     public static function provideDataVote(): iterable | ||||
|     { | ||||
|         yield [ | ||||
| @@ -107,17 +120,4 @@ class StoredObjectVoterTest extends TestCase | ||||
|             VoterInterface::ACCESS_GRANTED, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface | ||||
|     { | ||||
|         $storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class); | ||||
|         $storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports') | ||||
|             ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class)) | ||||
|             ->willReturn($supports); | ||||
|         $storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute') | ||||
|             ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class)) | ||||
|             ->willReturn($voteOnAttribute); | ||||
|  | ||||
|         return $storedObjectVoter; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,29 +40,6 @@ class RemoveOldVersionCronJobTest extends KernelTestCase | ||||
|         self::assertEquals($expected, $cronJob->canRun($cronJobExecution)); | ||||
|     } | ||||
|  | ||||
|     public static function buildTestCanRunData(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))), | ||||
|             true, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))), | ||||
|             true, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))), | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             null, | ||||
|             true, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function testRun(): void | ||||
|     { | ||||
|         // we create a clock in the future. This led us a chance to having stored object to delete | ||||
| @@ -86,6 +63,29 @@ class RemoveOldVersionCronJobTest extends KernelTestCase | ||||
|         self::assertIsInt($results['last-deleted-stored-object-version-id']); | ||||
|     } | ||||
|  | ||||
|     public static function buildTestCanRunData(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))), | ||||
|             true, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))), | ||||
|             true, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             (new CronJobExecution('last-deleted-stored-object-version-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))), | ||||
|             false, | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             null, | ||||
|             true, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function buildMessageBus(bool $expectDistpatchAtLeastOnce = false): MessageBusInterface | ||||
|     { | ||||
|         $messageBus = $this->createMock(MessageBusInterface::class); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user