mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 17:28:23 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			2.18.2
			...
			fix-compil
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 88c6e0e0d3 | 
| @@ -1,3 +0,0 @@ | ||||
| ## v2.16.2 - 2024-02-21 | ||||
| ### Fixed | ||||
| * Check for null values in closing motive of parcours d'accompagnement for correct rendering of template  | ||||
| @@ -1,5 +0,0 @@ | ||||
| ## v2.16.3 - 2024-02-26 | ||||
| ### Fixed | ||||
| * ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'  | ||||
| ### UX | ||||
| * ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters  | ||||
| @@ -1,9 +0,0 @@ | ||||
| ## v2.17.0 - 2024-03-19 | ||||
| ### Feature | ||||
| * ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates  | ||||
| * ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course  | ||||
| * ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields  | ||||
| * ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage | ||||
| ### Fixed | ||||
| * ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill  | ||||
| * ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period  | ||||
| @@ -1,5 +0,0 @@ | ||||
| ## v2.18.0 - 2024-03-26 | ||||
| ### Feature | ||||
| * ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation  | ||||
| ### Fixed | ||||
| * ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job  | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v2.18.1 - 2024-03-26 | ||||
| ### Fixed | ||||
| * Fix layout issue in document generation for admin (minor)  | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v2.18.2 - 2024-04-12 | ||||
| ### Fixed | ||||
| * ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record  | ||||
| @@ -23,7 +23,3 @@ max_line_length = 0 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
|  | ||||
| [.rst] | ||||
| ident_size = 3 | ||||
| ident_style = space | ||||
|  | ||||
|   | ||||
| @@ -35,7 +35,7 @@ variables: | ||||
|     # force a timezone | ||||
|     TZ: Europe/Brussels | ||||
|     # avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations | ||||
|     SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1 | ||||
|     SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=0 | ||||
|  | ||||
| stages: | ||||
|     - Composer install | ||||
|   | ||||
							
								
								
									
										34
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,40 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v2.18.2 - 2024-04-12 | ||||
| ### Fixed | ||||
| * ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record  | ||||
|  | ||||
| ## v2.18.1 - 2024-03-26 | ||||
| ### Fixed | ||||
| * Fix layout issue in document generation for admin (minor)  | ||||
|  | ||||
| ## v2.18.0 - 2024-03-26 | ||||
| ### Feature | ||||
| * ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation  | ||||
| ### Fixed | ||||
| * ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job  | ||||
|  | ||||
| ## v2.17.0 - 2024-03-19 | ||||
| ### Feature | ||||
| * ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates  | ||||
| * ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course  | ||||
| * ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields  | ||||
| * ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage | ||||
| ### Fixed | ||||
| * ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill  | ||||
| * ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period  | ||||
|  | ||||
| ## v2.16.3 - 2024-02-26 | ||||
| ### Fixed | ||||
| * ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'  | ||||
| ### UX | ||||
| * ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters  | ||||
|  | ||||
| ## v2.16.2 - 2024-02-21 | ||||
| ### Fixed | ||||
| * Check for null values in closing motive of parcours d'accompagnement for correct rendering of template  | ||||
|  | ||||
| ## v2.16.1 - 2024-02-09 | ||||
| ### Fixed | ||||
| * Force bootstrap version to avoid error in builds with newer version  | ||||
|   | ||||
| @@ -242,129 +242,3 @@ This is an example of the *filter by birthdate*. This filter asks some informati | ||||
|    Continue to explain the export framework | ||||
|  | ||||
| .. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main | ||||
|  | ||||
|  | ||||
| With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ? | ||||
| `````````````````````````````````````````````````````````````````````````````````````````````````````` | ||||
|  | ||||
| As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course | ||||
| which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on | ||||
| two accompanying period only: the ones with id 329 and 334. | ||||
|  | ||||
| Let's see the list of activities associated with those accompanying period: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7 | ||||
|        ORDER BY accompanyingperiod_id; | ||||
|  | ||||
| We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one. | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: id, accompanyingperiod_id, type_id | ||||
|  | ||||
|    990,329,7 | ||||
|    986,329,7 | ||||
|    987,329,7 | ||||
|    993,329,7 | ||||
|    991,329,7 | ||||
|    992,329,7 | ||||
|    1000,334,7 | ||||
|  | ||||
| Let's calculate the average duration for those accompanying periods, and the number of period: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334); | ||||
|  | ||||
| The result of this query is: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: AVG, COUNT | ||||
|  | ||||
|    2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2 | ||||
|  | ||||
| Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep | ||||
| only the accompanying period which contains the given activity type: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|                  JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|                  WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| What are the results here ? | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    7 | ||||
|  | ||||
| :code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which | ||||
| are taken into account for the computation: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period | ||||
|    JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: accompanyingperiod.id, activity.id | ||||
|  | ||||
|    329,993 | ||||
|    334,1000 | ||||
|    329,987 | ||||
|    329,990 | ||||
|    329,991 | ||||
|    329,992 | ||||
|    329,986 | ||||
|  | ||||
| For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we | ||||
| count one entry for each activity (actually, we count the number of activities). | ||||
|  | ||||
| So, let's use the :code:`DISTINCT` keyword to count only once the equal ids: | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|    SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|    JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; | ||||
|  | ||||
| Now, it works again... | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    2 | ||||
|  | ||||
| But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and | ||||
| :code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result. | ||||
|  | ||||
| The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|    SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period | ||||
|    WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); | ||||
|  | ||||
| The result is correct without :code:`DISTINCT` keyword: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: COUNT | ||||
|  | ||||
|    2 | ||||
|  | ||||
| And we can now compute the average duration without fear: | ||||
|  | ||||
| .. code-block:: sql | ||||
|  | ||||
|   SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period | ||||
|   WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); | ||||
|  | ||||
| Give the result: | ||||
|  | ||||
| .. csv-table:: | ||||
|    :header: AVG | ||||
|  | ||||
|    2 years 2 mons 21 days 12 hours 0 mins 0.0 secs | ||||
|   | ||||
| @@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface | ||||
|     { | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choices' => $this->userJobRepository->findAllOrderedByName(), | ||||
|                 'class' => UserJob::class, | ||||
|                 'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getLabel() | ||||
|   | ||||
| @@ -15,7 +15,6 @@ use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User\UserScopeHistory; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -27,8 +26,7 @@ class CreatorScopeFilter implements FilterInterface | ||||
|     private const PREFIX = 'acp_act_filter_creator_scope'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper, | ||||
|         private readonly ScopeRepositoryInterface $scopeRepository, | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -78,7 +76,6 @@ class CreatorScopeFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('scopes', EntityType::class, [ | ||||
|                 'class' => Scope::class, | ||||
|                 'choices' => $this->scopeRepository->findAllActive(), | ||||
|                 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getName() | ||||
|                 ), | ||||
|   | ||||
| @@ -16,7 +16,6 @@ use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -28,8 +27,7 @@ class UsersJobFilter implements FilterInterface | ||||
|     private const PREFIX = 'act_filter_user_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly UserJobRepositoryInterface $userJobRepository | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -71,7 +69,6 @@ class UsersJobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), | ||||
|                 'multiple' => true, | ||||
|                 'expanded' => true, | ||||
|   | ||||
| @@ -95,7 +95,7 @@ class ActivityType extends AbstractType | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         /** @var AccompanyingPeriod|null $accompanyingPeriod */ | ||||
|         /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */ | ||||
|         $accompanyingPeriod = null; | ||||
|  | ||||
|         if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) { | ||||
|   | ||||
| @@ -243,8 +243,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos | ||||
|             thirdparties.thirdpartyids, | ||||
|             persons.personids, | ||||
|             actions.socialactionids, | ||||
|             issues.socialissueids, | ||||
|             a.user_id | ||||
|             issues.socialissueids | ||||
|  | ||||
|         FROM activity a | ||||
|         LEFT JOIN chill_main_location location ON a.location_id = location.id | ||||
| @@ -284,7 +283,6 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos | ||||
|             ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') | ||||
|             ->addFieldResult('activityPresence', 'presence_id', 'id') | ||||
|             ->addFieldResult('activityPresence', 'presence_name', 'name') | ||||
|             ->addScalarResult('user_id', 'userId', Types::INTEGER) | ||||
|  | ||||
|             // results which cannot be mapped into entity | ||||
|             ->addScalarResult('comment_comment', 'comment', Types::TEXT) | ||||
|   | ||||
| @@ -11,7 +11,6 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Service\DocGenerator; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityPresence; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; | ||||
| @@ -113,7 +112,7 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list<Activity> | ||||
|      * @return list | ||||
|      */ | ||||
|     private function filterActivitiesByUser(array $activities, User $user): array | ||||
|     { | ||||
| @@ -121,12 +120,6 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|             array_filter( | ||||
|                 $activities, | ||||
|                 function ($activity) use ($user) { | ||||
|                     $u = $activity['user']; | ||||
|  | ||||
|                     if (null !== $u && $u['username'] === $user->getUsername()) { | ||||
|                         return true; | ||||
|                     } | ||||
|  | ||||
|                     $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); | ||||
|  | ||||
|                     return \in_array($user->getUsername(), $activityUsernames, true); | ||||
| @@ -136,7 +129,7 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list<AccompanyingPeriod\AccompanyingPeriodWork> | ||||
|      * @return list | ||||
|      */ | ||||
|     private function filterWorksByUser(array $works, User $user): array | ||||
|     { | ||||
| @@ -223,15 +216,6 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|         foreach ($activities as $row) { | ||||
|             $activity = $row[0]; | ||||
|  | ||||
|             $user = match (null === $row['userId']) { | ||||
|                 false => $this->userRepository->find($row['userId']), | ||||
|                 true => null, | ||||
|             }; | ||||
|  | ||||
|             $activity['user'] = $this->normalizer->normalize($user, 'docgen', [ | ||||
|                 AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class, | ||||
|             ]); | ||||
|  | ||||
|             $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [ | ||||
|                 AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class, | ||||
|             ]); | ||||
|   | ||||
| @@ -91,29 +91,6 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testfindByAccompanyingPeriodSimplified(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByAccompanyingPeriodSimplified($period); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
| @@ -324,10 +301,7 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             $job = new UserJob(); | ||||
|             $job->setLabel(['fr' => 'test']); | ||||
|             $this->entityManager->persist($job); | ||||
|             $this->entityManager->flush(); | ||||
|             throw new \RuntimeException('no jobs found'); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|   | ||||
| @@ -1,139 +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\ActivityBundle\Tests\Service\DocGenerator; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Service\DocGenerator\ListActivitiesByAccompanyingPeriodContext; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ListActivitiesByAccompanyingPeriodContextTest extends KernelTestCase | ||||
| { | ||||
|     private ListActivitiesByAccompanyingPeriodContext $listActivitiesByAccompanyingPeriodContext; | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|     private UserRepositoryInterface $userRepository; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class); | ||||
|         $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); | ||||
|         $this->userRepository = self::$container->get(UserRepositoryInterface::class); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void | ||||
|     { | ||||
|         $context = $this->getContext(); | ||||
|         $template = new DocGeneratorTemplate(); | ||||
|         $template->setOptions([ | ||||
|             'mainPerson' => false, | ||||
|             'person1' => false, | ||||
|             'person2' => false, | ||||
|             'thirdParty' => false, | ||||
|         ]); | ||||
|  | ||||
|         $data = $context->getData( | ||||
|             $template, | ||||
|             $this->accompanyingPeriodRepository->find($accompanyingPeriodId), | ||||
|             ['myActivitiesOnly' => false, 'myWorksOnly' => false] | ||||
|         ); | ||||
|  | ||||
|         self::assertIsArray($data); | ||||
|         self::assertArrayHasKey('activities', $data); | ||||
|         self::assertIsArray($data['activities']); | ||||
|         self::assertGreaterThan(0, count($data['activities'])); | ||||
|         self::assertIsArray($data['activities'][0]); | ||||
|         self::assertArrayHasKey('user', $data['activities'][0]); | ||||
|         self::assertIsArray($data['activities'][0]['user']); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideAccompanyingPeriod | ||||
|      */ | ||||
|     public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void | ||||
|     { | ||||
|         $context = $this->getContext(); | ||||
|         $template = new DocGeneratorTemplate(); | ||||
|         $template->setOptions([ | ||||
|             'mainPerson' => false, | ||||
|             'person1' => false, | ||||
|             'person2' => false, | ||||
|             'thirdParty' => false, | ||||
|         ]); | ||||
|  | ||||
|         $data = $context->getData( | ||||
|             $template, | ||||
|             $this->accompanyingPeriodRepository->find($accompanyingPeriodId), | ||||
|             ['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)] | ||||
|         ); | ||||
|  | ||||
|         self::assertIsArray($data); | ||||
|         self::assertArrayHasKey('activities', $data); | ||||
|         self::assertIsArray($data['activities']); | ||||
|         self::assertGreaterThan(0, count($data['activities'])); | ||||
|         self::assertIsArray($data['activities'][0]); | ||||
|         self::assertArrayHasKey('user', $data['activities'][0]); | ||||
|         self::assertIsArray($data['activities'][0]['user']); | ||||
|     } | ||||
|  | ||||
|     public static function provideAccompanyingPeriod(): array | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $em = self::$container->get(EntityManagerInterface::class); | ||||
|  | ||||
|         if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException('no period found'); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException('no user found'); | ||||
|         } | ||||
|  | ||||
|         $activity = new Activity(); | ||||
|         $activity | ||||
|             ->setAccompanyingPeriod($period) | ||||
|             ->setUser($user) | ||||
|             ->setDate(new \DateTime()); | ||||
|  | ||||
|         $em->persist($activity); | ||||
|         $em->flush(); | ||||
|  | ||||
|         self::ensureKernelShutdown(); | ||||
|  | ||||
|         return [ | ||||
|             [$period->getId(), $user->getId()], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function getContext(): ListActivitiesByAccompanyingPeriodContext | ||||
|     { | ||||
|         return $this->listActivitiesByAccompanyingPeriodContext; | ||||
|     } | ||||
| } | ||||
| @@ -396,7 +396,7 @@ export: | ||||
|             by_creator_job: | ||||
|                 job_form_label: Métiers | ||||
|                 Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange | ||||
|                 'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%" | ||||
|                 'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%" | ||||
|             by_persons: | ||||
|                 Filter activity by persons: Filtrer les échanges par usager participant | ||||
|                 'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%' | ||||
|   | ||||
| @@ -16,7 +16,6 @@ use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -28,8 +27,7 @@ class ByUserJobFilter implements FilterInterface | ||||
|     private const PREFIX = 'aside_act_filter_user_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly UserJobRepositoryInterface $userJobRepository | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -71,7 +69,6 @@ class ByUserJobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('jobs', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), | ||||
|                 'multiple' => true, | ||||
|                 'expanded' => true, | ||||
|   | ||||
| @@ -15,7 +15,6 @@ use Chill\CalendarBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\User\UserJobHistory; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\UserJobRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -27,8 +26,7 @@ final readonly class JobFilter implements FilterInterface | ||||
|     private const PREFIX = 'cal_filter_job'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private TranslatableStringHelper $translatableStringHelper, | ||||
|         private UserJobRepositoryInterface $userJobRepository | ||||
|         private TranslatableStringHelper $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -76,7 +74,6 @@ final readonly class JobFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('job', EntityType::class, [ | ||||
|                 'class' => UserJob::class, | ||||
|                 'choices' => $this->userJobRepository->findAllActive(), | ||||
|                 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( | ||||
|                     $j->getLabel() | ||||
|                 ), | ||||
|   | ||||
| @@ -15,7 +15,6 @@ use Chill\CalendarBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User\UserScopeHistory; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -29,8 +28,7 @@ class ScopeFilter implements FilterInterface | ||||
|  | ||||
|     public function __construct( | ||||
|         protected TranslatorInterface $translator, | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper, | ||||
|         private readonly ScopeRepositoryInterface $scopeRepository | ||||
|         private readonly TranslatableStringHelper $translatableStringHelper | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
| @@ -78,7 +76,6 @@ class ScopeFilter implements FilterInterface | ||||
|         $builder | ||||
|             ->add('scope', EntityType::class, [ | ||||
|                 'class' => Scope::class, | ||||
|                 'choices' => $this->scopeRepository->findAllActive(), | ||||
|                 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( | ||||
|                     $s->getName() | ||||
|                 ), | ||||
|   | ||||
| @@ -33,7 +33,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface | ||||
|     /** | ||||
|      * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft | ||||
|      */ | ||||
|     public function isUserAbsent(User $user): ?bool | ||||
|     public function isUserAbsent(User $user): bool|null | ||||
|     { | ||||
|         $id = $this->mapCalendarToUser->getUserId($user); | ||||
|  | ||||
|   | ||||
| @@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface | ||||
|     /** | ||||
|      * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft | ||||
|      */ | ||||
|     public function isUserAbsent(User $user): ?bool; | ||||
|     public function isUserAbsent(User $user): bool|null; | ||||
| } | ||||
|   | ||||
| @@ -16,42 +16,29 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; | ||||
| use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\EmailType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FileType; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| // TODO à mettre dans services | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||||
| use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||
| use Symfony\Component\Messenger\MessageBusInterface; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| use Symfony\Component\Validator\Constraints\NotNull; | ||||
|  | ||||
| final class DocGeneratorTemplateController extends AbstractController | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly ContextManager $contextManager, | ||||
|         private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         private readonly MessageBusInterface $messageBus, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly ClockInterface $clock, | ||||
|         private readonly Security $security, | ||||
|     ) { | ||||
|     public function __construct(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -176,7 +163,9 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|             throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)); | ||||
|         } | ||||
|  | ||||
|         $contextGenerationData = []; | ||||
|         $contextGenerationData = [ | ||||
|             'test_file' => null, | ||||
|         ]; | ||||
|  | ||||
|         if ( | ||||
|             $context instanceof DocGeneratorContextWithPublicFormInterface | ||||
| @@ -186,39 +175,25 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|                 $builder = $this->createFormBuilder( | ||||
|                     array_merge( | ||||
|                         $context->getFormData($template, $entity), | ||||
|                         $isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : [] | ||||
|                         $isTest ? ['test_file' => null, 'show_data' => false] : [] | ||||
|                     ) | ||||
|                 ); | ||||
|  | ||||
|                 $context->buildPublicForm($builder, $template, $entity); | ||||
|             } else { | ||||
|                 $builder = $this->createFormBuilder( | ||||
|                     ['creator' => null, 'show_data' => false, 'send_result_to' => ''] | ||||
|                     ['test_file' => null, 'show_data' => false] | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             if ($isTest) { | ||||
|                 $builder->add('dump_only', CheckboxType::class, [ | ||||
|                     'label' => 'docgen.Show data instead of generating', | ||||
|                 $builder->add('test_file', FileType::class, [ | ||||
|                     'label' => 'Template file', | ||||
|                     'required' => false, | ||||
|                 ]); | ||||
|                 $builder->add('send_result_to', EmailType::class, [ | ||||
|                     'label' => 'docgen.Send report to', | ||||
|                     'help' => 'docgen.Send report errors to this email address', | ||||
|                     'empty_data' => '', | ||||
|                     'required' => true, | ||||
|                     'constraints' => [ | ||||
|                         new NotBlank(), | ||||
|                         new NotNull(), | ||||
|                     ], | ||||
|                 ]); | ||||
|                 $builder->add('creator', PickUserDynamicType::class, [ | ||||
|                     'label' => 'docgen.Generate as creator', | ||||
|                     'help' => 'docgen.The document will be generated as the given creator', | ||||
|                     'multiple' => false, | ||||
|                     'constraints' => [ | ||||
|                         new NotNull(), | ||||
|                     ], | ||||
|                 $builder->add('show_data', CheckboxType::class, [ | ||||
|                     'label' => 'Show data instead of generating', | ||||
|                     'required' => false, | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
| @@ -229,10 +204,8 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|             } elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) { | ||||
|                 $templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig'; | ||||
|                 $templateOptions = [ | ||||
|                     'entity' => $entity, | ||||
|                     'form' => $form->createView(), | ||||
|                     'template' => $template, | ||||
|                     'context' => $context, | ||||
|                     'entity' => $entity, 'form' => $form->createView(), | ||||
|                     'template' => $template, 'context' => $context, | ||||
|                 ]; | ||||
|  | ||||
|                 return $this->render($templatePath, $templateOptions); | ||||
| @@ -245,57 +218,60 @@ final class DocGeneratorTemplateController extends AbstractController | ||||
|                 $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) | ||||
|                 : []; | ||||
|  | ||||
|         // if is test, render the data or generate the doc | ||||
|         if ($isTest && isset($form) && $form['show_data']->getData()) { | ||||
|             return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ | ||||
|                 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), \JSON_PRETTY_PRINT), | ||||
|             ]); | ||||
|         } | ||||
|         if ($isTest) { | ||||
|             $generated = $this->generator->generateDocFromTemplate( | ||||
|                 $template, | ||||
|                 $entityId, | ||||
|                 $contextGenerationDataSanitized, | ||||
|                 null, | ||||
|                 true, | ||||
|                 isset($form) ? $form['test_file']->getData() : null | ||||
|             ); | ||||
|  | ||||
|             return new Response( | ||||
|                 $generated, | ||||
|                 Response::HTTP_OK, | ||||
|                 [ | ||||
|                     'Content-Transfer-Encoding', 'binary', | ||||
|                     'Content-Type' => 'application/vnd.oasis.opendocument.text', | ||||
|                     'Content-Disposition' => 'attachment; filename="generated.odt"', | ||||
|                     'Content-Length' => \strlen($generated), | ||||
|                 ], | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // this is not a test | ||||
|         // we prepare the object to store the document | ||||
|         $storedObject = (new StoredObject()) | ||||
|             ->setStatus(StoredObject::STATUS_PENDING) | ||||
|         ; | ||||
|  | ||||
|         if ($isTest) { | ||||
|             // document will be stored during 15 days, if generation is a test | ||||
|             $storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D'))); | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->persist($storedObject); | ||||
|  | ||||
|         // we store the generated document (associate with the original entity, etc.) | ||||
|         // but only if this is not a test | ||||
|         if (!$isTest) { | ||||
|             $context | ||||
|                 ->storeGenerated( | ||||
|                     $template, | ||||
|                     $storedObject, | ||||
|                     $entity, | ||||
|                     $contextGenerationData | ||||
|                 ); | ||||
|         } | ||||
|         // we store the generated document | ||||
|         $context | ||||
|             ->storeGenerated( | ||||
|                 $template, | ||||
|                 $storedObject, | ||||
|                 $entity, | ||||
|                 $contextGenerationData | ||||
|             ); | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|  | ||||
|         if ($isTest) { | ||||
|             $creator = $contextGenerationData['creator']; | ||||
|             $sendResultTo =  ($form ?? null)?->get('send_result_to')?->getData() ?? null; | ||||
|             $dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false; | ||||
|         } else { | ||||
|             $creator = $this->security->getUser(); | ||||
|  | ||||
|             if (!$creator instanceof User) { | ||||
|                 throw new AccessDeniedHttpException('only authenticated user can request a generation'); | ||||
|             } | ||||
|  | ||||
|             $sendResultTo = null; | ||||
|             $dumpOnly = false; | ||||
|         } | ||||
|  | ||||
|         $this->messageBus->dispatch( | ||||
|             new RequestGenerationMessage( | ||||
|                 $creator, | ||||
|                 $this->getUser(), | ||||
|                 $template, | ||||
|                 $entityId, | ||||
|                 $storedObject, | ||||
|                 $contextGenerationDataSanitized, | ||||
|                 $isTest, | ||||
|                 $sendResultTo, | ||||
|                 $dumpOnly, | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|   | ||||
| @@ -69,7 +69,7 @@ class DocGeneratorTemplate | ||||
|      * | ||||
|      * @Serializer\Groups({"read"}) | ||||
|      */ | ||||
|     private ?int $id = null; | ||||
|     private int $id; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="json") | ||||
|   | ||||
| @@ -14,9 +14,10 @@ namespace Chill\DocGeneratorBundle\Repository; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
|  | ||||
| final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface | ||||
| final class DocGeneratorTemplateRepository implements ObjectRepository | ||||
| { | ||||
|     private readonly EntityRepository $repository; | ||||
|  | ||||
|   | ||||
| @@ -1,23 +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\Repository; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @extends ObjectRepository<DocGeneratorTemplate> | ||||
|  */ | ||||
| interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository | ||||
| { | ||||
|     public function countByEntity(string $entity): int; | ||||
| } | ||||
| @@ -1,62 +1,36 @@ | ||||
| {% extends '@ChillMain/CRUD/Admin/index.html.twig' %} | ||||
|  | ||||
| {% block js %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_script_tags('mod_document_action_buttons_group') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block css %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_link_tags('mod_document_action_buttons_group') }} | ||||
| {% endblock %} | ||||
|  | ||||
|  | ||||
| {% block admin_content %} | ||||
|     {% embed '@ChillMain/CRUD/_index.html.twig' %} | ||||
|         {% block table_entities_thead_tr %} | ||||
|             <th></th> | ||||
|             <th>{{ 'Title'|trans }}</th> | ||||
|             <th>{{ 'docgen.Context'|trans }}</th> | ||||
|             <th>{{ 'docgen.test generate'|trans }}</th> | ||||
|             <th>{{ 'Edit'|trans }}</th> | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block table_entities_tbody %} | ||||
|             {% if entities|length == 0 %} | ||||
|                 <p class="chill-no-data-statement">{{ 'docgen.Any template configured'|trans }}</p> | ||||
|             {% else %} | ||||
|                 <div class="flex-table"> | ||||
|                     {% for entity in entities %} | ||||
|                             <div class="item-bloc"> | ||||
|                                 <div class="item-row"> | ||||
|                                     <div class="item-col" style="flex-basis:100%;"> | ||||
|                                         <h2>{{ entity.name|localize_translatable_string }}</h2> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                                 <div class="item-row"> | ||||
|                                     <p><span class="badge bg-chill-green-dark">{{ contextManager.getContextByKey(entity.context).name|trans }}</span></p> | ||||
|                                 </div> | ||||
|                                 <div class="item-row"> | ||||
|                                     <div class="item-col"></div> | ||||
|                                     <ul class="record_actions item-col flex-shrink-1"> | ||||
|                                         <li> | ||||
|                                             <form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}"> | ||||
|                                                 <input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', app.request.uri)|e('html_attr') }}" /> | ||||
|                                                 <input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" /> | ||||
|                                                 <input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" /> | ||||
|                                                 <input type="text" name="entityId" placeholder="{{ 'docgen.entity_id_placeholder'|trans }}" required /> | ||||
|  | ||||
|                                                 <button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button> | ||||
|                                             </form> | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             {{ entity.file|chill_document_button_group('Template file', true) }} | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             <a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a> | ||||
|                                         </li> | ||||
|                                     </ul> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                     {% endfor %} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             {% for entity in entities %} | ||||
|                 <tr> | ||||
|                     <td>{{ entity.id }}</td> | ||||
|                     <td>{{ entity.name|localize_translatable_string}}</td> | ||||
|                     <td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td> | ||||
|                     <td> | ||||
|                         <form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}"> | ||||
|                             <input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', '/')|e('html_attr') }}" /> | ||||
|                             <input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" /> | ||||
|                             <input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" /> | ||||
|                             <input type="text" name="entityId" /> | ||||
|  | ||||
|                             <button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button> | ||||
|                         </form> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             {% endfor %} | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block actions_before %} | ||||
|   | ||||
| @@ -6,20 +6,18 @@ | ||||
| <div class="col-md-10 col-xxl"> | ||||
|  | ||||
|     <h1>{{ block('title') }}</h1> | ||||
|    <div class="container overflow-hidden"> | ||||
|    <div class="container"> | ||||
|        {% for key, context in contexts %} | ||||
|             <div class="row g-3" style="margin-top: 1rem;"> | ||||
|                 <div class="col-4 offset-1 text-center"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-4"> | ||||
|                     <a | ||||
|                         href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}" | ||||
|                         class="btn btn-outline-chill-green-dark"> | ||||
|                         {{ context.name|trans }} | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div class="col"> | ||||
|                     <div> | ||||
|                         {{ context.description|trans|nl2br }} | ||||
|                     </div> | ||||
|                 <div class="col-md-8"> | ||||
|                    {{ context.description|trans|nl2br }} | ||||
|                 </div> | ||||
|             </div> | ||||
|        {% endfor %} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {% if creator is not same as null %}{{ creator.label }},{% endif %} | ||||
| {{ creator.label }}, | ||||
|  | ||||
| {{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }} | ||||
| {{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} | ||||
|  | ||||
| {{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} | ||||
|  | ||||
|   | ||||
| @@ -1,7 +0,0 @@ | ||||
| {{ 'docgen.data_dump_email.Dear'|trans }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }} | ||||
|  | ||||
| {{ link }} | ||||
|  | ||||
| {{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }} | ||||
| @@ -17,88 +17,54 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; | ||||
| use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\Yaml\Yaml; | ||||
| use Symfony\Component\HttpFoundation\File\File; | ||||
|  | ||||
| class Generator implements GeneratorInterface | ||||
| { | ||||
|     private const LOG_PREFIX = '[docgen generator] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly ContextManagerInterface $contextManager, | ||||
|         private readonly DriverInterface $driver, | ||||
|         private readonly ManagerRegistry $objectManagerRegistry, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly StoredObjectManagerInterface $storedObjectManager | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function generateDataDump( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject { | ||||
|         return $this->generateFromTemplate( | ||||
|             $template, | ||||
|             $entityId, | ||||
|             $contextGenerationDataNormalized, | ||||
|             $destinationStoredObject, | ||||
|             $creator, | ||||
|             $clearEntityManagerDuringProcess, | ||||
|             true, | ||||
|         ); | ||||
|     public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @template T of File|null | ||||
|      * @template B of bool | ||||
|      * | ||||
|      * @param B                      $isTest | ||||
|      * @param (B is true ? T : null) $testFile | ||||
|      * | ||||
|      * @psalm-return (B is true ? string : null) | ||||
|      * | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable | ||||
|      */ | ||||
|     public function generateDocFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject { | ||||
|         return $this->generateFromTemplate( | ||||
|             $template, | ||||
|             $entityId, | ||||
|             $contextGenerationDataNormalized, | ||||
|             $destinationStoredObject, | ||||
|             $creator, | ||||
|             $clearEntityManagerDuringProcess, | ||||
|             false, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function generateFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|         bool $generateDumpOnly = false, | ||||
|     ): StoredObject { | ||||
|         if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { | ||||
|         ?StoredObject $destinationStoredObject = null, | ||||
|         bool $isTest = false, | ||||
|         ?File $testFile = null, | ||||
|         ?User $creator = null | ||||
|     ): ?string { | ||||
|         if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); | ||||
|             throw new ObjectReadyException(); | ||||
|         } | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ | ||||
|             'entity_id' => $entityId, | ||||
|             'destination_stored_object' => $destinationStoredObject->getId(), | ||||
|             'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), | ||||
|         ]); | ||||
|  | ||||
|         $context = $this->contextManager->getContextByDocGeneratorTemplate($template); | ||||
|  | ||||
|         $entity = $this | ||||
|             ->objectManagerRegistry | ||||
|             ->getManagerForClass($context->getEntityClass()) | ||||
|             ->entityManager | ||||
|             ->find($context->getEntityClass(), $entityId) | ||||
|         ; | ||||
|  | ||||
| @@ -116,47 +82,17 @@ class Generator implements GeneratorInterface | ||||
|  | ||||
|         $data = $context->getData($template, $entity, $contextGenerationDataNormalized); | ||||
|  | ||||
|         $destinationStoredObjectId = $destinationStoredObject->getId(); | ||||
|  | ||||
|         if ($clearEntityManagerDuringProcess) { | ||||
|             // we clean the entity manager | ||||
|             $this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear(); | ||||
|  | ||||
|             // this will force php to clean the memory | ||||
|             gc_collect_cycles(); | ||||
|         $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; | ||||
|         $this->entityManager->clear(); | ||||
|         gc_collect_cycles(); | ||||
|         if (null !== $destinationStoredObjectId) { | ||||
|             $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); | ||||
|         } | ||||
|  | ||||
|         // as we potentially deleted the storedObject from memory, we have to restore it | ||||
|         $destinationStoredObject = $this->objectManagerRegistry | ||||
|             ->getManagerForClass(StoredObject::class) | ||||
|             ->find(StoredObject::class, $destinationStoredObjectId); | ||||
|  | ||||
|         if ($generateDumpOnly) { | ||||
|             $content = Yaml::dump($data, 6); | ||||
|             /* @var StoredObject $destinationStoredObject */ | ||||
|             $destinationStoredObject | ||||
|                 ->setType('application/yaml') | ||||
|                 ->setFilename(sprintf('%s_yaml', uniqid('doc_', true))) | ||||
|                 ->setStatus(StoredObject::STATUS_READY) | ||||
|             ; | ||||
|  | ||||
|             try { | ||||
|                 $this->storedObjectManager->write($destinationStoredObject, $content); | ||||
|             } catch (StoredObjectManagerException $e) { | ||||
|                 $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|  | ||||
|                 throw new GeneratorException([$e->getMessage()], $e); | ||||
|             } | ||||
|  | ||||
|             return $destinationStoredObject; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|         if ($isTest && ($testFile instanceof File)) { | ||||
|             $templateDecrypted = file_get_contents($testFile->getPathname()); | ||||
|         } else { | ||||
|             $templateDecrypted = $this->storedObjectManager->read($template->getFile()); | ||||
|         } catch (StoredObjectManagerException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|  | ||||
|             throw new GeneratorException([$e->getMessage()], $e); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
| @@ -169,10 +105,19 @@ class Generator implements GeneratorInterface | ||||
|                     $template->getFile()->getFilename() | ||||
|                 ); | ||||
|         } catch (TemplateException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors())); | ||||
|             throw new GeneratorException($e->getErrors(), $e); | ||||
|         } | ||||
|  | ||||
|         if (true === $isTest) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ | ||||
|                 'is_test' => true, | ||||
|                 'entity_id' => $entityId, | ||||
|                 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), | ||||
|             ]); | ||||
|  | ||||
|             return $generatedResource; | ||||
|         } | ||||
|  | ||||
|         /* @var StoredObject $destinationStoredObject */ | ||||
|         $destinationStoredObject | ||||
|             ->setType($template->getFile()->getType()) | ||||
| @@ -180,19 +125,15 @@ class Generator implements GeneratorInterface | ||||
|             ->setStatus(StoredObject::STATUS_READY) | ||||
|         ; | ||||
|  | ||||
|         try { | ||||
|             $this->storedObjectManager->write($destinationStoredObject, $generatedResource); | ||||
|         } catch (StoredObjectManagerException $e) { | ||||
|             $destinationStoredObject->addGenerationErrors($e->getMessage()); | ||||
|         $this->storedObjectManager->write($destinationStoredObject, $generatedResource); | ||||
|  | ||||
|             throw new GeneratorException([$e->getMessage()], $e); | ||||
|         } | ||||
|         $this->entityManager->flush(); | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ | ||||
|             'entity_id' => $entityId, | ||||
|             'destination_stored_object' => $destinationStoredObject->getId(), | ||||
|         ]); | ||||
|  | ||||
|         return $destinationStoredObject; | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,48 +13,29 @@ namespace Chill\DocGeneratorBundle\Service\Generator; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Symfony\Component\HttpFoundation\File\File; | ||||
|  | ||||
| interface GeneratorInterface | ||||
| { | ||||
|     /** | ||||
|      * Generate a document and store the document on disk. | ||||
|      * @template T of File|null | ||||
|      * @template B of bool | ||||
|      * | ||||
|      * The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored | ||||
|      * into the object. The number of generation trial will also be incremented. | ||||
|      * @param B                      $isTest | ||||
|      * @param (B is true ? T : null) $testFile | ||||
|      * | ||||
|      * This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process, | ||||
|      * unless the paarameter `$clearEntityManagerDuringProcess` is set on false. | ||||
|      * @psalm-return (B is true ? string : null) | ||||
|      * | ||||
|      * As the entity manager might be cleaned, the new instance of the stored object will be returned by this method. | ||||
|      * | ||||
|      * Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`). | ||||
|      * | ||||
|      * @phpstan-impure | ||||
|      * | ||||
|      * @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials | ||||
|      * | ||||
|      * @throws StoredObjectManagerException if unable to decrypt the template or store the document | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable | ||||
|      */ | ||||
|     public function generateDocFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject; | ||||
|  | ||||
|     /** | ||||
|      * Generate a data dump, and store it within the `$destinationStoredObject`. | ||||
|      */ | ||||
|     public function generateDataDump( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int $entityId, | ||||
|         array $contextGenerationDataNormalized, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         User $creator, | ||||
|         bool $clearEntityManagerDuringProcess = true, | ||||
|     ): StoredObject; | ||||
|         ?StoredObject $destinationStoredObject = null, | ||||
|         bool $isTest = false, | ||||
|         ?File $testFile = null, | ||||
|         ?User $creator = null | ||||
|     ): ?string; | ||||
| } | ||||
|   | ||||
| @@ -1,64 +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\Service\Messenger; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; | ||||
|  | ||||
| /** | ||||
|  * The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache | ||||
|  * after a specific message is handled or fails. | ||||
|  */ | ||||
| final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private StoredObjectManagerInterface $storedObjectManager, | ||||
|         private LoggerInterface $logger, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public static function getSubscribedEvents() | ||||
|     { | ||||
|         return [ | ||||
|             WorkerMessageHandledEvent::class => [ | ||||
|                 ['afterHandling', 0], | ||||
|             ], | ||||
|             WorkerMessageFailedEvent::class => [ | ||||
|                 ['afterFails', 0], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function afterHandling(WorkerMessageHandledEvent $event): void | ||||
|     { | ||||
|         if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|             $this->clearStoredObjectCache(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function afterFails(WorkerMessageFailedEvent $event): void | ||||
|     { | ||||
|         if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|             $this->clearStoredObjectCache(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private function clearStoredObjectCache(): void | ||||
|     { | ||||
|         $this->logger->debug('clear the cache after generation of a document'); | ||||
|  | ||||
|         $this->storedObjectManager->clearCache(); | ||||
|     } | ||||
| } | ||||
| @@ -11,11 +11,10 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| @@ -25,22 +24,12 @@ use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * @see OnGenerationFailsTest for test suite | ||||
|  */ | ||||
| final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
| { | ||||
|     public const LOG_PREFIX = '[docgen failed] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository, | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private LoggerInterface $logger, | ||||
|         private MailerInterface $mailer, | ||||
|         private StoredObjectRepositoryInterface $storedObjectRepository, | ||||
|         private TranslatorInterface $translator, | ||||
|         private UserRepositoryInterface $userRepository | ||||
|     ) { | ||||
|     public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public static function getSubscribedEvents() | ||||
| @@ -56,12 +45,13 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $message = $event->getEnvelope()->getMessage(); | ||||
|  | ||||
|         if (!$message instanceof RequestGenerationMessage) { | ||||
|         if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         /** @var RequestGenerationMessage $message */ | ||||
|         $message = $event->getEnvelope()->getMessage(); | ||||
|  | ||||
|         $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ | ||||
|             'stored_object_id' => $message->getDestinationStoredObjectId(), | ||||
|             'entity_id' => $message->getEntityId(), | ||||
| @@ -89,8 +79,16 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|  | ||||
|     private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void | ||||
|     { | ||||
|         if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'No email associated with this request generation'); | ||||
|         $creatorId = $message->getCreatorId(); | ||||
|  | ||||
|         if (null === $creator = $this->userRepository->find($creatorId)) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (null === $creator->getEmail() || '' === $creator->getEmail()) { | ||||
|             $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
| @@ -98,7 +96,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|         // if the exception is not a GeneratorException, we try the previous one... | ||||
|         $throwable = $event->getThrowable(); | ||||
|         if (!$throwable instanceof GeneratorException) { | ||||
|             $throwable = $throwable->getPrevious() ?? $throwable; | ||||
|             $throwable = $throwable->getPrevious(); | ||||
|         } | ||||
|  | ||||
|         if ($throwable instanceof GeneratorException) { | ||||
| @@ -113,14 +111,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (null === $creator = $this->userRepository->find($message->getCreatorId())) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Creator not found'); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $email = (new TemplatedEmail()) | ||||
|             ->to($message->getSendResultToEmail()) | ||||
|             ->to($creator->getEmail()) | ||||
|             ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) | ||||
|             ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') | ||||
|             ->context([ | ||||
|   | ||||
| @@ -11,21 +11,15 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\Generator; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Bridge\Twig\Mime\TemplatedEmail; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; | ||||
| use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * Handle the request of document generation. | ||||
| @@ -36,17 +30,8 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|  | ||||
|     private const LOG_PREFIX = '[docgen message handler] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly Generator $generator, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly StoredObjectRepository $storedObjectRepository, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|         private readonly MailerInterface $mailer, | ||||
|         private readonly TempUrlGeneratorInterface $tempUrlGenerator, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|     ) { | ||||
|     public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public function __invoke(RequestGenerationMessage $message) | ||||
| @@ -60,59 +45,25 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|         } | ||||
|  | ||||
|         if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [ | ||||
|                 'template_id' => $message->getTemplateId(), | ||||
|                 'destination_stored_object' => $message->getDestinationStoredObjectId(), | ||||
|                 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), | ||||
|             ]); | ||||
|  | ||||
|             throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); | ||||
|         } | ||||
|  | ||||
|         $creator = $this->userRepository->find($message->getCreatorId()); | ||||
|  | ||||
|         // we increase the number of generation trial in the object, and, in the same time, update the counter | ||||
|         // on the database side. This ensure that, if the script fails for any reason (memory limit reached), the | ||||
|         // counter is inscreased | ||||
|         $destinationStoredObject->addGenerationTrial(); | ||||
|         $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') | ||||
|             ->setParameter('id', $destinationStoredObject->getId()) | ||||
|             ->execute(); | ||||
|  | ||||
|         try { | ||||
|             if ($message->isDumpOnly()) { | ||||
|                 $destinationStoredObject = $this->generator->generateDataDump( | ||||
|                     $template, | ||||
|                     $message->getEntityId(), | ||||
|                     $message->getContextGenerationData(), | ||||
|                     $destinationStoredObject, | ||||
|                     $creator | ||||
|                 ); | ||||
|  | ||||
|                 $this->sendDataDump($destinationStoredObject, $message); | ||||
|             } else { | ||||
|                 $destinationStoredObject = $this->generator->generateDocFromTemplate( | ||||
|                     $template, | ||||
|                     $message->getEntityId(), | ||||
|                     $message->getContextGenerationData(), | ||||
|                     $destinationStoredObject, | ||||
|                     $creator | ||||
|                 ); | ||||
|             } | ||||
|         } catch (StoredObjectManagerException|GeneratorException $e) { | ||||
|             $this->entityManager->flush(); | ||||
|  | ||||
|             $this->logger->error(self::LOG_PREFIX.'Request generation failed', [ | ||||
|                 'template_id' => $message->getTemplateId(), | ||||
|                 'destination_stored_object' => $message->getDestinationStoredObjectId(), | ||||
|                 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), | ||||
|                 'error' => $e->getTraceAsString(), | ||||
|             ]); | ||||
|  | ||||
|             throw $e; | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|         $this->generator->generateDocFromTemplate( | ||||
|             $template, | ||||
|             $message->getEntityId(), | ||||
|             $message->getContextGenerationData(), | ||||
|             $destinationStoredObject, | ||||
|             false, | ||||
|             null, | ||||
|             $creator | ||||
|         ); | ||||
|  | ||||
|         $this->logger->info(self::LOG_PREFIX.'Request generation finished', [ | ||||
|             'template_id' => $message->getTemplateId(), | ||||
| @@ -120,23 +71,4 @@ class RequestGenerationHandler implements MessageHandlerInterface | ||||
|             'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void | ||||
|     { | ||||
|         $url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600); | ||||
|         $parts = []; | ||||
|         parse_str(parse_url((string) $url->url)['query'], $parts); | ||||
|         $validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']); | ||||
|  | ||||
|         $email = (new TemplatedEmail()) | ||||
|             ->to($message->getSendResultToEmail()) | ||||
|             ->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig') | ||||
|             ->context([ | ||||
|                 'link' => $url->url, | ||||
|                 'validity' => $validity, | ||||
|             ]) | ||||
|             ->subject($this->translator->trans('docgen.data_dump_email.subject')); | ||||
|  | ||||
|         $this->mailer->send($email); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,33 +15,27 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\MainBundle\Entity\User; | ||||
|  | ||||
| final readonly class RequestGenerationMessage | ||||
| class RequestGenerationMessage | ||||
| { | ||||
|     private int $creatorId; | ||||
|     private readonly int $creatorId; | ||||
|  | ||||
|     private int $templateId; | ||||
|     private readonly int $templateId; | ||||
|  | ||||
|     private int $destinationStoredObjectId; | ||||
|     private readonly int $destinationStoredObjectId; | ||||
|  | ||||
|     private \DateTimeImmutable $createdAt; | ||||
|  | ||||
|     private ?string $sendResultToEmail; | ||||
|     private readonly \DateTimeImmutable $createdAt; | ||||
|  | ||||
|     public function __construct( | ||||
|         User $creator, | ||||
|         DocGeneratorTemplate $template, | ||||
|         private int $entityId, | ||||
|         private readonly int $entityId, | ||||
|         StoredObject $destinationStoredObject, | ||||
|         private array $contextGenerationData, | ||||
|         private bool $isTest = false, | ||||
|         ?string $sendResultToEmail = null, | ||||
|         private bool $dumpOnly = false, | ||||
|         private readonly array $contextGenerationData | ||||
|     ) { | ||||
|         $this->creatorId = $creator->getId(); | ||||
|         $this->templateId = $template->getId(); | ||||
|         $this->destinationStoredObjectId = $destinationStoredObject->getId(); | ||||
|         $this->createdAt = new \DateTimeImmutable('now'); | ||||
|         $this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail(); | ||||
|     } | ||||
|  | ||||
|     public function getCreatorId(): int | ||||
| @@ -73,19 +67,4 @@ final readonly class RequestGenerationMessage | ||||
|     { | ||||
|         return $this->createdAt; | ||||
|     } | ||||
|  | ||||
|     public function isTest(): bool | ||||
|     { | ||||
|         return $this->isTest; | ||||
|     } | ||||
|  | ||||
|     public function getSendResultToEmail(): ?string | ||||
|     { | ||||
|         return $this->sendResultToEmail; | ||||
|     } | ||||
|  | ||||
|     public function isDumpOnly(): bool | ||||
|     { | ||||
|         return $this->dumpOnly; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -20,9 +20,7 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| @@ -68,11 +66,7 @@ class GeneratorTest extends TestCase | ||||
|         $entityManager->find('DummyClass', Argument::type('int')) | ||||
|             ->willReturn($entity); | ||||
|         $entityManager->clear()->shouldBeCalled(); | ||||
|         $entityManager->flush()->shouldNotBeCalled(); | ||||
|  | ||||
|         $managerRegistry = $this->prophesize(ManagerRegistry::class); | ||||
|         $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); | ||||
|         $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); | ||||
|         $entityManager->flush()->shouldBeCalled(); | ||||
|  | ||||
|         $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storedObjectManager->read($templateStoredObject)->willReturn('template'); | ||||
| @@ -81,7 +75,7 @@ class GeneratorTest extends TestCase | ||||
|         $generator = new Generator( | ||||
|             $contextManagerInterface->reveal(), | ||||
|             $driver->reveal(), | ||||
|             $managerRegistry->reveal(), | ||||
|             $entityManager->reveal(), | ||||
|             new NullLogger(), | ||||
|             $storedObjectManager->reveal() | ||||
|         ); | ||||
| @@ -90,8 +84,7 @@ class GeneratorTest extends TestCase | ||||
|             $template, | ||||
|             1, | ||||
|             [], | ||||
|             $destinationStoredObject, | ||||
|             new User() | ||||
|             $destinationStoredObject | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @@ -102,7 +95,7 @@ class GeneratorTest extends TestCase | ||||
|         $generator = new Generator( | ||||
|             $this->prophesize(ContextManagerInterface::class)->reveal(), | ||||
|             $this->prophesize(DriverInterface::class)->reveal(), | ||||
|             $this->prophesize(ManagerRegistry::class)->reveal(), | ||||
|             $this->prophesize(EntityManagerInterface::class)->reveal(), | ||||
|             new NullLogger(), | ||||
|             $this->prophesize(StoredObjectManagerInterface::class)->reveal() | ||||
|         ); | ||||
| @@ -115,8 +108,7 @@ class GeneratorTest extends TestCase | ||||
|             $template, | ||||
|             1, | ||||
|             [], | ||||
|             $destinationStoredObject, | ||||
|             new User() | ||||
|             $destinationStoredObject | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @@ -144,14 +136,10 @@ class GeneratorTest extends TestCase | ||||
|         $entityManager->find(Argument::type('string'), Argument::type('int')) | ||||
|             ->willReturn(null); | ||||
|  | ||||
|         $managerRegistry = $this->prophesize(ManagerRegistry::class); | ||||
|         $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); | ||||
|         $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); | ||||
|  | ||||
|         $generator = new Generator( | ||||
|             $contextManagerInterface->reveal(), | ||||
|             $this->prophesize(DriverInterface::class)->reveal(), | ||||
|             $managerRegistry->reveal(), | ||||
|             $entityManager->reveal(), | ||||
|             new NullLogger(), | ||||
|             $this->prophesize(StoredObjectManagerInterface::class)->reveal() | ||||
|         ); | ||||
| @@ -160,8 +148,7 @@ class GeneratorTest extends TestCase | ||||
|             $template, | ||||
|             1, | ||||
|             [], | ||||
|             $destinationStoredObject, | ||||
|             new User() | ||||
|             $destinationStoredObject | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,107 +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\Service\Messenger\OnAfterMessageHandledClearStoredObjectCache; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class OnAfterMessageHandledClearStoredObjectCacheTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testThatNotGenerationMessageDoesNotCallAClearCache(): void | ||||
|     { | ||||
|         $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storedObjectManager->clearCache()->shouldNotBeCalled(); | ||||
|  | ||||
|         $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); | ||||
|  | ||||
|         $eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass())); | ||||
|         $eventSubscriber->afterFails($this->buildEventFailed(new \stdClass())); | ||||
|     } | ||||
|  | ||||
|     public function testThatConcernedEventCallAClearCache(): void | ||||
|     { | ||||
|         $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storedObjectManager->clearCache()->shouldBeCalledTimes(2); | ||||
|  | ||||
|         $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); | ||||
|  | ||||
|         $eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage())); | ||||
|         $eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage())); | ||||
|     } | ||||
|  | ||||
|     private function buildRequestGenerationMessage( | ||||
|     ): RequestGenerationMessage { | ||||
|         $creator = new User(); | ||||
|         $creator->setEmail('fake@example.com'); | ||||
|  | ||||
|         $class = new \ReflectionClass($creator); | ||||
|         $property = $class->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($creator, 1); | ||||
|  | ||||
|         $template ??= new DocGeneratorTemplate(); | ||||
|         $class = new \ReflectionClass($template); | ||||
|         $property = $class->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($template, 2); | ||||
|  | ||||
|         $destinationStoredObject = new StoredObject(); | ||||
|         $class = new \ReflectionClass($destinationStoredObject); | ||||
|         $property = $class->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($destinationStoredObject, 3); | ||||
|  | ||||
|         return new RequestGenerationMessage( | ||||
|             $creator, | ||||
|             $template, | ||||
|             1, | ||||
|             $destinationStoredObject, | ||||
|             [], | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache | ||||
|     { | ||||
|         return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger()); | ||||
|     } | ||||
|  | ||||
|     private function buildEventFailed(object $message): WorkerMessageFailedEvent | ||||
|     { | ||||
|         $envelope = new Envelope($message); | ||||
|  | ||||
|         return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); | ||||
|     } | ||||
|  | ||||
|     private function buildEventSuccess(object $message): WorkerMessageHandledEvent | ||||
|     { | ||||
|         $envelope = new Envelope($message); | ||||
|  | ||||
|         return new WorkerMessageHandledEvent($envelope, 'test_receiver'); | ||||
|     } | ||||
| } | ||||
| @@ -1,226 +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\Messenger\OnGenerationFails; | ||||
| use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Component\Mime\Email; | ||||
| use Symfony\Component\Mime\RawMessage; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class OnGenerationFailsTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testNotConcernedMessageAreNotHandled(): void | ||||
|     { | ||||
|         $entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $entityManager->flush()->shouldNotBeCalled(); | ||||
|  | ||||
|         $mailer = $this->prophesize(MailerInterface::class); | ||||
|         $mailer->send()->shouldNotBeCalled(); | ||||
|  | ||||
|         $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( | ||||
|             entityManager: $entityManager->reveal(), | ||||
|             mailer: $mailer->reveal() | ||||
|         ); | ||||
|  | ||||
|         $event = $this->buildEvent(new \stdClass()); | ||||
|  | ||||
|         $eventSubscriber->onMessageFailed($event); | ||||
|     } | ||||
|  | ||||
|     public function testMessageThatWillBeRetriedAreNotHandled(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|  | ||||
|         $entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $entityManager->flush()->shouldNotBeCalled(); | ||||
|  | ||||
|         $mailer = $this->prophesize(MailerInterface::class); | ||||
|         $mailer->send()->shouldNotBeCalled(); | ||||
|  | ||||
|         $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( | ||||
|             entityManager: $entityManager->reveal(), | ||||
|             mailer: $mailer->reveal() | ||||
|         ); | ||||
|  | ||||
|         $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); | ||||
|         $event->setForRetry(); | ||||
|  | ||||
|         $eventSubscriber->onMessageFailed($event); | ||||
|     } | ||||
|  | ||||
|     public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|  | ||||
|         $entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $entityManager->flush()->shouldBeCalled(); | ||||
|  | ||||
|         $mailer = $this->prophesize(MailerInterface::class); | ||||
|         $mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled(); | ||||
|  | ||||
|         $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( | ||||
|             entityManager: $entityManager->reveal(), | ||||
|             mailer: $mailer->reveal(), | ||||
|             storedObject: $storedObject | ||||
|         ); | ||||
|  | ||||
|         $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); | ||||
|  | ||||
|         $eventSubscriber->onMessageFailed($event); | ||||
|  | ||||
|         self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus()); | ||||
|     } | ||||
|  | ||||
|     public function testThatANonRetryableEventSendAnEmail(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|  | ||||
|         $entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $entityManager->flush()->shouldBeCalled(); | ||||
|  | ||||
|         $mailer = $this->prophesize(MailerInterface::class); | ||||
|         $mailer->send( | ||||
|             Argument::that(function ($arg): bool { | ||||
|                 if (!$arg instanceof Email) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 foreach ($arg->getTo() as $to) { | ||||
|                     if ('test@test.com' === $to->getAddress()) { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
|             }), | ||||
|             Argument::any() | ||||
|         ) | ||||
|             ->shouldBeCalled(); | ||||
|  | ||||
|         $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( | ||||
|             entityManager: $entityManager->reveal(), | ||||
|             mailer: $mailer->reveal(), | ||||
|             storedObject: $storedObject | ||||
|         ); | ||||
|  | ||||
|         $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com')); | ||||
|  | ||||
|         $eventSubscriber->onMessageFailed($event); | ||||
|     } | ||||
|  | ||||
|     private function buildRequestGenerationMessage( | ||||
|         StoredObject $destinationStoredObject, | ||||
|         ?User $creator = null, | ||||
|         ?DocGeneratorTemplate $template = null, | ||||
|         array $contextGenerationData = [], | ||||
|         bool $isTest = false, | ||||
|         ?string $sendResultToEmail = null, | ||||
|     ): RequestGenerationMessage { | ||||
|         if (null === $creator) { | ||||
|             $creator = new User(); | ||||
|             $creator->setEmail('fake@example.com'); | ||||
|         } | ||||
|  | ||||
|         if (null === $creator->getId()) { | ||||
|             $class = new \ReflectionClass($creator); | ||||
|             $property = $class->getProperty('id'); | ||||
|             $property->setAccessible(true); | ||||
|             $property->setValue($creator, 1); | ||||
|         } | ||||
|  | ||||
|         $template ??= new DocGeneratorTemplate(); | ||||
|         $class = new \ReflectionClass($template); | ||||
|         $property = $class->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($template, 2); | ||||
|  | ||||
|         $class = new \ReflectionClass($destinationStoredObject); | ||||
|         $property = $class->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($destinationStoredObject, 3); | ||||
|  | ||||
|         return new RequestGenerationMessage( | ||||
|             $creator, | ||||
|             $template, | ||||
|             1, | ||||
|             $destinationStoredObject, | ||||
|             $contextGenerationData, | ||||
|             $isTest, | ||||
|             $sendResultToEmail | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function buildOnGenerationFailsEventSubscriber( | ||||
|         ?StoredObject $storedObject = null, | ||||
|         ?EntityManagerInterface $entityManager = null, | ||||
|         ?MailerInterface $mailer = null, | ||||
|     ): OnGenerationFails { | ||||
|         $storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class); | ||||
|         $storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject()); | ||||
|  | ||||
|         if (null === $entityManager) { | ||||
|             $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); | ||||
|         } | ||||
|  | ||||
|         if (null === $mailer) { | ||||
|             $mailerProphecy = $this->prophesize(MailerInterface::class); | ||||
|         } | ||||
|  | ||||
|         $translator = $this->prophesize(TranslatorInterface::class); | ||||
|         $translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]); | ||||
|  | ||||
|         $userRepository = $this->prophesize(UserRepositoryInterface::class); | ||||
|         $userRepository->find(Argument::type('int'))->willReturn(new User()); | ||||
|  | ||||
|         $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); | ||||
|         $docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate()); | ||||
|  | ||||
|         return new OnGenerationFails( | ||||
|             $docGeneratorTemplateRepository->reveal(), | ||||
|             $entityManager ?? $entityManagerProphecy->reveal(), | ||||
|             new NullLogger(), | ||||
|             $mailer ?? $mailerProphecy->reveal(), | ||||
|             $storedObjectRepository->reveal(), | ||||
|             $translator->reveal(), | ||||
|             $userRepository->reveal() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function buildEvent(object $message): WorkerMessageFailedEvent | ||||
|     { | ||||
|         $envelope = new Envelope($message); | ||||
|  | ||||
|         return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +0,0 @@ | ||||
| docgen: | ||||
|     data_dump_email: | ||||
|         link_valid_until: >- | ||||
|             Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium} | ||||
| @@ -14,31 +14,13 @@ docgen: | ||||
|     Doc generation is pending: La génération de ce document est en cours | ||||
|     Come back later: Revenir plus tard | ||||
|  | ||||
|     Send report to: Envoyer le rapport à | ||||
|     Send report errors to this email address: Les rapports d'erreurs seront envoyés à l'adresse email indiquée | ||||
|     Generate as creator: Générer en tant que | ||||
|     The document will be generated as the given creator: Le document sera généré à la place de l'utilisateur indiqué | ||||
|     Show data instead of generating: Montrer les données au lieu de générer le document | ||||
|  | ||||
|     Any template configured: Aucun gabarit de document configuré | ||||
|  | ||||
|     entity_id_placeholder: Identifiant de l'entité | ||||
|  | ||||
|     failure_email: | ||||
|         The generation of a document failed: La génération d'un document a échoué | ||||
|         The generation of the document %template_name% failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. | ||||
|         The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. | ||||
|         The following errors were encoutered: Les erreurs suivantes ont été rencontrées | ||||
|         Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème. | ||||
|         References: Références | ||||
|  | ||||
|     data_dump_email: | ||||
|         subject: Contenu des données de génération de document disponible | ||||
|         Dear: Cher | ||||
|         data_dump_ready_and_link: >- | ||||
|             Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant: | ||||
|  | ||||
|  | ||||
|  | ||||
| crud: | ||||
|     docgen_template: | ||||
|         index: | ||||
| @@ -46,4 +28,5 @@ crud: | ||||
|             add_new: Créer | ||||
|  | ||||
|  | ||||
| Show data instead of generating: Montrer les données au lieu de générer le document | ||||
| Template file: Fichier modèle | ||||
|   | ||||
| @@ -25,11 +25,6 @@ use Symfony\Component\Serializer\Annotation as Serializer; | ||||
| /** | ||||
|  * Represent a document stored in an object store. | ||||
|  * | ||||
|  * StoredObjects 's content should be read and written using the @see{StoredObjectManagerInterface}. | ||||
|  * | ||||
|  * The property `$deleteAt` allow a deletion of the document after the given date. But this property should | ||||
|  * be set before the document is actually written by the StoredObjectManager. | ||||
|  * | ||||
|  * @ORM\Entity | ||||
|  * | ||||
|  * @ORM\Table("chill_doc.stored_object") | ||||
| @@ -122,16 +117,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa | ||||
|      */ | ||||
|     private int $generationTrialsCounter = 0; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null}) | ||||
|      */ | ||||
|     private ?\DateTimeImmutable $deleteAt = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="text", nullable=false, options={"default": ""}) | ||||
|      */ | ||||
|     private string $generationErrors = ''; | ||||
|  | ||||
|     /** | ||||
|      * @param StoredObject::STATUS_* $status | ||||
|      */ | ||||
| @@ -159,11 +144,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa | ||||
|      */ | ||||
|     public function getCreationDate(): \DateTime | ||||
|     { | ||||
|         if (null === $this->createdAt) { | ||||
|             // this scenario will quite never happens | ||||
|             return new \DateTime('now'); | ||||
|         } | ||||
|  | ||||
|         return \DateTime::createFromImmutable($this->createdAt); | ||||
|     } | ||||
|  | ||||
| @@ -323,37 +303,4 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa | ||||
|     { | ||||
|         return self::STATUS_FAILURE === $this->getStatus(); | ||||
|     } | ||||
|  | ||||
|     public function getDeleteAt(): ?\DateTimeImmutable | ||||
|     { | ||||
|         return $this->deleteAt; | ||||
|     } | ||||
|  | ||||
|     public function setDeleteAt(?\DateTimeImmutable $deleteAt): StoredObject | ||||
|     { | ||||
|         $this->deleteAt = $deleteAt; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getGenerationErrors(): string | ||||
|     { | ||||
|         return $this->generationErrors; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds generation errors to the stored object. | ||||
|      * | ||||
|      * The existing generation errors are not removed | ||||
|      * | ||||
|      * @param string $generationErrors the generation errors to be added | ||||
|      * | ||||
|      * @return StoredObject the modified StoredObject instance | ||||
|      */ | ||||
|     public function addGenerationErrors(string $generationErrors): StoredObject | ||||
|     { | ||||
|         $this->generationErrors = $this->generationErrors.$generationErrors."\n"; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,10 +14,11 @@ namespace Chill\DocStoreBundle\Repository; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface | ||||
| final class StoredObjectRepository implements ObjectRepository | ||||
| { | ||||
|     private EntityRepository $repository; | ||||
|     private readonly EntityRepository $repository; | ||||
|  | ||||
|     public function __construct(EntityManagerInterface $entityManager) | ||||
|     { | ||||
|   | ||||
| @@ -1,22 +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\Repository; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @extends ObjectRepository<StoredObject> | ||||
|  */ | ||||
| interface StoredObjectRepositoryInterface extends ObjectRepository | ||||
| { | ||||
| } | ||||
| @@ -104,12 +104,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface | ||||
|             ) | ||||
|             : $clearContent; | ||||
|  | ||||
|         $headers = []; | ||||
|  | ||||
|         if (null !== $document->getDeleteAt()) { | ||||
|             $headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp(); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             $response = $this | ||||
|                 ->client | ||||
| @@ -124,7 +118,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface | ||||
|                         ->url, | ||||
|                     [ | ||||
|                         'body' => $encryptedContent, | ||||
|                         'headers' => $headers, | ||||
|                     ] | ||||
|                 ); | ||||
|         } catch (TransportExceptionInterface $exception) { | ||||
| @@ -136,11 +129,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function clearCache(): void | ||||
|     { | ||||
|         $this->inMemory = []; | ||||
|     } | ||||
|  | ||||
|     private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable | ||||
|     { | ||||
|         $lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? ''); | ||||
|   | ||||
| @@ -12,7 +12,6 @@ declare(strict_types=1); | ||||
| namespace Chill\DocStoreBundle\Service; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
|  | ||||
| interface StoredObjectManagerInterface | ||||
| { | ||||
| @@ -24,8 +23,6 @@ interface StoredObjectManagerInterface | ||||
|      * @param StoredObject $document the document | ||||
|      * | ||||
|      * @return string the retrieved content in clear | ||||
|      * | ||||
|      * @throws StoredObjectManagerException if unable to read or decrypt the content | ||||
|      */ | ||||
|     public function read(StoredObject $document): string; | ||||
|  | ||||
| @@ -34,10 +31,6 @@ interface StoredObjectManagerInterface | ||||
|      * | ||||
|      * @param StoredObject $document     the document | ||||
|      * @param              $clearContent The content to store in clear | ||||
|      * | ||||
|      * @throws StoredObjectManagerException | ||||
|      */ | ||||
|     public function write(StoredObject $document, string $clearContent): void; | ||||
|  | ||||
|     public function clearCache(): void; | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ declare(strict_types=1); | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
| 
 | ||||
| namespace Chill\DocStoreBundle\Tests\Service; | ||||
| namespace Chill\DocStoreBundle\Tests; | ||||
| 
 | ||||
| use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| @@ -117,41 +117,6 @@ final class StoredObjectManagerTest extends TestCase | ||||
|         self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); | ||||
|     } | ||||
| 
 | ||||
|     public function testWriteWithDeleteAt() | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
| 
 | ||||
|         $expectedRequests = [ | ||||
|             function ($method, $url, $options): MockResponse { | ||||
|                 self::assertEquals('PUT', $method); | ||||
|                 self::assertArrayHasKey('headers', $options); | ||||
|                 self::assertIsArray($options['headers']); | ||||
|                 self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); | ||||
| 
 | ||||
|                 return new MockResponse('', ['http_code' => 201]); | ||||
|             }, | ||||
| 
 | ||||
|             function ($method, $url, $options): MockResponse { | ||||
|                 self::assertEquals('PUT', $method); | ||||
|                 self::assertArrayHasKey('headers', $options); | ||||
|                 self::assertIsArray($options['headers']); | ||||
|                 self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); | ||||
|                 self::assertContains('X-Delete-At: 1711014260', $options['headers']); | ||||
| 
 | ||||
|                 return new MockResponse('', ['http_code' => 201]); | ||||
|             }, | ||||
|         ]; | ||||
|         $client = new MockHttpClient($expectedRequests); | ||||
| 
 | ||||
|         $manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); | ||||
| 
 | ||||
|         $manager->write($storedObject, 'ok'); | ||||
| 
 | ||||
|         // with a deletedAt date
 | ||||
|         $storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260')); | ||||
|         $manager->write($storedObject, 'ok'); | ||||
|     } | ||||
| 
 | ||||
|     private function getHttpClient(string $encodedContent): HttpClientInterface | ||||
|     { | ||||
|         $callback = static function ($method, $url, $options) use ($encodedContent) { | ||||
| @@ -1,36 +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\DocStore; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240322100107 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'StoredObject: add deleteAt and generationErrors columns'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL'); | ||||
|         $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\''); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors'); | ||||
|     } | ||||
| } | ||||
| @@ -433,7 +433,6 @@ final class EventController extends AbstractController | ||||
|         $builder->add('event_id', HiddenType::class, [ | ||||
|             'data' => $event->getId(), | ||||
|         ]); | ||||
|         dump($event->getId()); | ||||
|  | ||||
|         return $builder->getForm(); | ||||
|     } | ||||
|   | ||||
| @@ -197,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getModerator(): ?User | ||||
|     public function getModerator(): User|null | ||||
|     { | ||||
|         return $this->moderator; | ||||
|     } | ||||
|   | ||||
| @@ -91,7 +91,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa | ||||
|     /** | ||||
|      * Get event. | ||||
|      */ | ||||
|     public function getEvent(): ?Event | ||||
|     public function getEvent(): Event|null | ||||
|     { | ||||
|         return $this->event; | ||||
|     } | ||||
| @@ -127,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa | ||||
|     /** | ||||
|      * Get role. | ||||
|      */ | ||||
|     public function getRole(): ?Role | ||||
|     public function getRole(): Role|null | ||||
|     { | ||||
|         return $this->role; | ||||
|     } | ||||
| @@ -147,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa | ||||
|     /** | ||||
|      * Get status. | ||||
|      */ | ||||
|     public function getStatus(): ?Status | ||||
|     public function getStatus(): Status|null | ||||
|     { | ||||
|         return $this->status; | ||||
|     } | ||||
|   | ||||
| @@ -12,12 +12,11 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Command; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Language; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityManager; | ||||
| use Symfony\Component\Console\Command\Command; | ||||
| use Symfony\Component\Console\Input\InputInterface; | ||||
| use Symfony\Component\Console\Input\InputOption; | ||||
| use Symfony\Component\Console\Output\OutputInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Intl\Languages; | ||||
|  | ||||
| /* | ||||
| @@ -40,7 +39,7 @@ class LoadAndUpdateLanguagesCommand extends Command | ||||
|     /** | ||||
|      * LoadCountriesCommand constructor. | ||||
|      */ | ||||
|     public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag) | ||||
|     public function __construct(private readonly EntityManager $entityManager, private $availableLanguages) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|     } | ||||
| @@ -80,7 +79,7 @@ class LoadAndUpdateLanguagesCommand extends Command | ||||
|     protected function execute(InputInterface $input, OutputInterface $output): int | ||||
|     { | ||||
|         $em = $this->entityManager; | ||||
|         $chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages'); | ||||
|         $chillAvailableLanguages = $this->availableLanguages; | ||||
|         $languages = []; | ||||
|  | ||||
|         foreach ($chillAvailableLanguages as $avLang) { | ||||
| @@ -114,7 +113,7 @@ class LoadAndUpdateLanguagesCommand extends Command | ||||
|             $avLangNames = []; | ||||
|  | ||||
|             foreach ($chillAvailableLanguages as $avLang) { | ||||
|                 $avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang)); | ||||
|                 $avLangNames[$avLang] = Languages::getName($code, $avLang); | ||||
|             } | ||||
|  | ||||
|             $languageDB->setName($avLangNames); | ||||
|   | ||||
| @@ -47,12 +47,4 @@ class AdminController extends AbstractController | ||||
|     { | ||||
|         return $this->render('@ChillMain/Admin/indexUser.html.twig'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin") | ||||
|      */ | ||||
|     public function indexDashboardAction() | ||||
|     { | ||||
|         return $this->render('@ChillMain/Admin/indexDashboard.html.twig'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,52 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\NewsItemRepository; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
|  | ||||
| final readonly class DashboardApiController | ||||
| { | ||||
|     public function __construct( | ||||
|         private NewsItemRepository $newsItemRepository, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get user dashboard config (not yet based on user id and still hardcoded for now). | ||||
|      * | ||||
|      * @Route("/api/1.0/main/dashboard-config-item.json", methods={"get"}) | ||||
|      */ | ||||
|     public function getDashboardConfiguration(): JsonResponse | ||||
|     { | ||||
|         $data = []; | ||||
|  | ||||
|         if (0 < $this->newsItemRepository->countCurrentNews()) { | ||||
|             // show news only if we have news | ||||
|             // NOTE: maybe this should be done in the frontend... | ||||
|             $data[] = | ||||
|                 [ | ||||
|                     'position' => 'top-left', | ||||
|                     'id' => 1, | ||||
|                     'type' => 'news', | ||||
|                     'metadata' => [ | ||||
|                         // arbitrary data that will be store "some time" | ||||
|                         'only_unread' => false, | ||||
|                     ], | ||||
|                 ]; | ||||
|         } | ||||
|  | ||||
|         return new JsonResponse($data, JsonResponse::HTTP_OK, []); | ||||
|     } | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Repository\NewsItemRepository; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Serializer\SerializerInterface; | ||||
|  | ||||
| class NewsItemApiController | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly NewsItemRepository $newsItemRepository, | ||||
|         private readonly SerializerInterface $serializer, | ||||
|         private readonly PaginatorFactory $paginatorFactory | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get list of news items filtered on start and end date. | ||||
|      * | ||||
|      * @Route("/api/1.0/main/news/current.json", methods={"get"}) | ||||
|      */ | ||||
|     public function listCurrentNewsItems(): JsonResponse | ||||
|     { | ||||
|         $total = $this->newsItemRepository->countCurrentNews(); | ||||
|         $paginator = $this->paginatorFactory->create($total); | ||||
|         $newsItems = $this->newsItemRepository->findCurrentNews( | ||||
|             $paginator->getItemsPerPage(), | ||||
|             $paginator->getCurrentPage()->getFirstItemNumber() | ||||
|         ); | ||||
|  | ||||
|         return new JsonResponse($this->serializer->serialize( | ||||
|             new Collection(array_values($newsItems), $paginator), | ||||
|             'json', | ||||
|             [ | ||||
|                 AbstractNormalizer::GROUPS => ['read'], | ||||
|             ] | ||||
|         ), JsonResponse::HTTP_OK, [], true); | ||||
|     } | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\CRUD\Controller\CRUDController; | ||||
| use Chill\MainBundle\Pagination\PaginatorInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| class NewsItemController extends CRUDController | ||||
| { | ||||
|     protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) | ||||
|     { | ||||
|         $query->addOrderBy('e.startDate', 'DESC'); | ||||
|         $query->addOrderBy('e.id', 'DESC'); | ||||
|  | ||||
|         return parent::orderQuery($action, $query, $request, $paginator); | ||||
|     } | ||||
| } | ||||
| @@ -1,73 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Entity\NewsItem; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Repository\NewsItemRepository; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Twig\Environment; | ||||
|  | ||||
| final readonly class NewsItemHistoryController | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly NewsItemRepository $newsItemRepository, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|         private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, | ||||
|         private readonly Environment $environment, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/{_locale}/news-items/history", name="chill_main_news_items_history") | ||||
|      */ | ||||
|     public function list(): Response | ||||
|     { | ||||
|         $filter = $this->buildFilterOrder(); | ||||
|         $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString()); | ||||
|         $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString()); | ||||
|  | ||||
|         $pagination = $this->paginatorFactory->create($total); | ||||
|  | ||||
|         return new Response($this->environment->render('@ChillMain/NewsItem/news_items_history.html.twig', [ | ||||
|             'entities' => $newsItems, | ||||
|             'paginator' => $pagination, | ||||
|             'filter_order' => $filter, | ||||
|         ])); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item") | ||||
|      */ | ||||
|     public function showSingleItem(NewsItem $newsItem, Request $request): Response | ||||
|     { | ||||
|         return new Response($this->environment->render( | ||||
|             '@ChillMain/NewsItem/show.html.twig', | ||||
|             [ | ||||
|                 'entity' => $newsItem, | ||||
|             ] | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     private function buildFilterOrder(): FilterOrderHelper | ||||
|     { | ||||
|         $filterBuilder = $this->filterOrderHelperFactory | ||||
|             ->create(self::class) | ||||
|             ->addSearchBox(); | ||||
|  | ||||
|         return $filterBuilder->build(); | ||||
|     } | ||||
| } | ||||
| @@ -28,5 +28,5 @@ interface CronJobInterface | ||||
|      * | ||||
|      * @return array|null optionally return an array with the same data than the previous execution | ||||
|      */ | ||||
|     public function run(array $lastExecutionData): ?array; | ||||
|     public function run(array $lastExecutionData): array|null; | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,6 @@ use Chill\MainBundle\Controller\CountryController; | ||||
| use Chill\MainBundle\Controller\LanguageController; | ||||
| use Chill\MainBundle\Controller\LocationController; | ||||
| use Chill\MainBundle\Controller\LocationTypeController; | ||||
| use Chill\MainBundle\Controller\NewsItemController; | ||||
| use Chill\MainBundle\Controller\RegroupmentController; | ||||
| use Chill\MainBundle\Controller\UserController; | ||||
| use Chill\MainBundle\Controller\UserJobApiController; | ||||
| @@ -54,7 +53,6 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer; | ||||
| use Chill\MainBundle\Entity\Language; | ||||
| use Chill\MainBundle\Entity\Location; | ||||
| use Chill\MainBundle\Entity\LocationType; | ||||
| use Chill\MainBundle\Entity\NewsItem; | ||||
| use Chill\MainBundle\Entity\Regroupment; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| @@ -64,7 +62,6 @@ use Chill\MainBundle\Form\CountryType; | ||||
| use Chill\MainBundle\Form\LanguageType; | ||||
| use Chill\MainBundle\Form\LocationFormType; | ||||
| use Chill\MainBundle\Form\LocationTypeType; | ||||
| use Chill\MainBundle\Form\NewsItemType; | ||||
| use Chill\MainBundle\Form\RegroupmentType; | ||||
| use Chill\MainBundle\Form\UserJobType; | ||||
| use Chill\MainBundle\Form\UserType; | ||||
| @@ -547,35 +544,6 @@ class ChillMainExtension extends Extension implements | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|                 [ | ||||
|                     'class' => NewsItem::class, | ||||
|                     'name' => 'news_item', | ||||
|                     'base_path' => '/admin/news_item', | ||||
|                     'form_class' => NewsItemType::class, | ||||
|                     'controller' => NewsItemController::class, | ||||
|                     'actions' => [ | ||||
|                         'index' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/NewsItem/index.html.twig', | ||||
|                         ], | ||||
|                         'new' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/NewsItem/new.html.twig', | ||||
|                         ], | ||||
|                         'view' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/NewsItem/view_admin.html.twig', | ||||
|                         ], | ||||
|                         'edit' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/NewsItem/edit.html.twig', | ||||
|                         ], | ||||
|                         'delete' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/NewsItem/delete.html.twig', | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|             ], | ||||
|             'apis' => [ | ||||
|                 [ | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -39,15 +40,15 @@ class Age extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->value1 = $parser->SimpleArithmeticExpression(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->value2 = $parser->SimpleArithmeticExpression(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Doctrine\DQL; | ||||
| use Doctrine\ORM\Query\AST\Functions\DateDiffFunction; | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\AST\PathExpression; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -44,17 +45,17 @@ class Extract extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $this->field = $parser->getLexer()->token['value']; | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_FROM); | ||||
|         $parser->match(Lexer::T_FROM); | ||||
|  | ||||
|         // $this->value = $parser->ScalarExpression(); | ||||
|         $this->value = $parser->ArithmeticPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -32,11 +33,11 @@ class GetJsonFieldByKey extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->expr1 = $parser->StringPrimary(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|         $this->expr2 = $parser->StringPrimary(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\AST\Node; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -40,15 +41,15 @@ class Greatest extends FunctionNode | ||||
|         $this->exprs = []; | ||||
|  | ||||
|         $lexer = $parser->getLexer(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|  | ||||
|         while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         while (Lexer::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(Lexer::T_COMMA); | ||||
|             $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|         } | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -32,9 +33,9 @@ class JsonAggregate extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->expr = $parser->StringPrimary(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\AST\Node; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -37,16 +38,16 @@ class JsonBuildObject extends FunctionNode | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $lexer = $parser->getLexer(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|  | ||||
|         while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         while (Lexer::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(Lexer::T_COMMA); | ||||
|             $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|         } | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -28,15 +29,15 @@ class JsonExtract extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->element = $parser->ArithmeticPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->keyToExtract = $parser->ArithmeticExpression(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -32,9 +33,9 @@ class JsonbArrayLength extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser): void | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->expr1 = $parser->StringPrimary(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -32,11 +33,11 @@ class JsonbExistsInArray extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser): void | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->expr1 = $parser->StringPrimary(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|         $this->expr2 = $parser->InputParameter(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\AST\Node; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -40,15 +41,15 @@ class Least extends FunctionNode | ||||
|         $this->exprs = []; | ||||
|  | ||||
|         $lexer = $parser->getLexer(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|  | ||||
|         while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         while (Lexer::T_COMMA === $lexer->lookahead['type']) { | ||||
|             $parser->match(Lexer::T_COMMA); | ||||
|             $this->exprs[] = $parser->ArithmeticPrimary(); | ||||
|         } | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\AST\PathExpression; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
|  | ||||
| /** | ||||
| @@ -44,29 +45,29 @@ class OverlapsI extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser): void | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->firstPeriodStart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->firstPeriodEnd = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->secondPeriodStart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->secondPeriodEnd = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
|  | ||||
|     protected function makeCase($sqlWalker, $part, string $position): string | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
|  | ||||
| class Replace extends FunctionNode | ||||
| { | ||||
| @@ -34,19 +35,19 @@ class Replace extends FunctionNode | ||||
|  | ||||
|     public function parse(\Doctrine\ORM\Query\Parser $parser): void | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->string = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->from = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->to = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
|  | ||||
| /** | ||||
|  * Geometry function 'ST_CONTAINS', added by postgis. | ||||
| @@ -30,15 +31,15 @@ class STContains extends FunctionNode | ||||
|  | ||||
|     public function parse(\Doctrine\ORM\Query\Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->firstPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->secondPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -26,11 +27,11 @@ class STX extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->field = $parser->ArithmeticExpression(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -26,11 +27,11 @@ class STY extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->field = $parser->ArithmeticExpression(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
|  | ||||
| class Similarity extends FunctionNode | ||||
| { | ||||
| @@ -27,15 +28,15 @@ class Similarity extends FunctionNode | ||||
|  | ||||
|     public function parse(\Doctrine\ORM\Query\Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->firstPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->secondPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -28,15 +29,15 @@ class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\Function | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->firstPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|  | ||||
|         $this->secondPart = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
| use Doctrine\ORM\Query\Parser; | ||||
| use Doctrine\ORM\Query\SqlWalker; | ||||
|  | ||||
| @@ -35,11 +36,11 @@ class ToChar extends FunctionNode | ||||
|  | ||||
|     public function parse(Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|         $this->datetime = $parser->ArithmeticExpression(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); | ||||
|         $parser->match(Lexer::T_COMMA); | ||||
|         $this->fmt = $parser->StringExpression(); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Doctrine\DQL; | ||||
|  | ||||
| use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||||
| use Doctrine\ORM\Query\Lexer; | ||||
|  | ||||
| /** | ||||
|  * Unaccent string using postgresql extension unaccent : | ||||
| @@ -30,11 +31,11 @@ class Unaccent extends FunctionNode | ||||
|  | ||||
|     public function parse(\Doctrine\ORM\Query\Parser $parser) | ||||
|     { | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_IDENTIFIER); | ||||
|         $parser->match(Lexer::T_OPEN_PARENTHESIS); | ||||
|  | ||||
|         $this->string = $parser->StringPrimary(); | ||||
|  | ||||
|         $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); | ||||
|         $parser->match(Lexer::T_CLOSE_PARENTHESIS); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,112 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Entity; | ||||
|  | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Component\Serializer\Annotation as Serializer; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
|  | ||||
| /** | ||||
|  * @ORM\Entity | ||||
|  * | ||||
|  * @ORM\Table(name="chill_main_dashboard_config_item") | ||||
|  */ | ||||
| class DashboardConfigItem | ||||
| { | ||||
|     /** | ||||
|      * @ORM\Id | ||||
|      * | ||||
|      * @ORM\GeneratedValue | ||||
|      * | ||||
|      * @ORM\Column(type="integer") | ||||
|      * | ||||
|      * @Serializer\Groups({"dashboardConfigItem:read", "read"}) | ||||
|      */ | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="string") | ||||
|      * | ||||
|      * @Serializer\Groups({"dashboardConfigItem:read", "read"}) | ||||
|      * | ||||
|      * @Assert\NotNull | ||||
|      */ | ||||
|     private string $type = ''; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="string") | ||||
|      * | ||||
|      * @Serializer\Groups({"dashboardConfigItem:read", "read"}) | ||||
|      * | ||||
|      * @Assert\NotNull | ||||
|      */ | ||||
|     private string $position = ''; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\ManyToOne(targetEntity=User::class) | ||||
|      */ | ||||
|     private ?User $user = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="json", options={"default": "[]", "jsonb": true}) | ||||
|      * | ||||
|      * @Serializer\Groups({"dashboardConfigItem:read"}) | ||||
|      */ | ||||
|     private array $metadata = []; | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getType(): string | ||||
|     { | ||||
|         return $this->type; | ||||
|     } | ||||
|  | ||||
|     public function setType(string $type): self | ||||
|     { | ||||
|         $this->type = $type; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getPosition(): string | ||||
|     { | ||||
|         return $this->position; | ||||
|     } | ||||
|  | ||||
|     public function setPosition(string $position): void | ||||
|     { | ||||
|         $this->position = $position; | ||||
|     } | ||||
|  | ||||
|     public function getUser(): User | ||||
|     { | ||||
|         return $this->user; | ||||
|     } | ||||
|  | ||||
|     public function setUser(User $user): void | ||||
|     { | ||||
|         $this->user = $user; | ||||
|     } | ||||
|  | ||||
|     public function getMetadata(): array | ||||
|     { | ||||
|         return $this->metadata; | ||||
|     } | ||||
|  | ||||
|     public function setMetadata(array $metadata): void | ||||
|     { | ||||
|         $this->metadata = $metadata; | ||||
|     } | ||||
| } | ||||
| @@ -1,128 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Component\Serializer\Annotation\Groups; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
|  | ||||
| /** | ||||
|  * @ORM\Entity | ||||
|  * | ||||
|  * @ORM\Table(name="chill_main_news") | ||||
|  */ | ||||
| class NewsItem implements TrackCreationInterface, TrackUpdateInterface | ||||
| { | ||||
|     use TrackCreationTrait; | ||||
|  | ||||
|     use TrackUpdateTrait; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Id | ||||
|      * | ||||
|      * @ORM\GeneratedValue | ||||
|      * | ||||
|      * @ORM\Column(type="integer") | ||||
|      * | ||||
|      * @Groups({"read"}) | ||||
|      */ | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="text") | ||||
|      * | ||||
|      * @Groups({"read"}) | ||||
|      * | ||||
|      * @Assert\NotBlank | ||||
|      * | ||||
|      * @Assert\NotNull | ||||
|      */ | ||||
|     private string $title = ''; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="text") | ||||
|      * | ||||
|      * @Groups({"read"}) | ||||
|      * | ||||
|      * @Assert\NotBlank | ||||
|      * | ||||
|      * @Assert\NotNull | ||||
|      */ | ||||
|     private string $content = ''; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="date_immutable", nullable=false) | ||||
|      * | ||||
|      * @Assert\NotNull | ||||
|      * | ||||
|      * @Groups({"read"}) | ||||
|      */ | ||||
|     private ?\DateTimeImmutable $startDate = null; | ||||
|  | ||||
|     /** | ||||
|      * @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) | ||||
|      * | ||||
|      * @Assert\GreaterThanOrEqual(propertyPath="startDate") | ||||
|      * | ||||
|      * @Groups({"read"}) | ||||
|      */ | ||||
|     private ?\DateTimeImmutable $endDate = null; | ||||
|  | ||||
|     public function getTitle(): string | ||||
|     { | ||||
|         return $this->title; | ||||
|     } | ||||
|  | ||||
|     public function setTitle(string $title): void | ||||
|     { | ||||
|         $this->title = $title; | ||||
|     } | ||||
|  | ||||
|     public function getContent(): string | ||||
|     { | ||||
|         return $this->content; | ||||
|     } | ||||
|  | ||||
|     public function setContent(string $content): void | ||||
|     { | ||||
|         $this->content = $content; | ||||
|     } | ||||
|  | ||||
|     public function getStartDate(): ?\DateTimeImmutable | ||||
|     { | ||||
|         return $this->startDate; | ||||
|     } | ||||
|  | ||||
|     public function setStartDate(?\DateTimeImmutable $startDate): void | ||||
|     { | ||||
|         $this->startDate = $startDate; | ||||
|     } | ||||
|  | ||||
|     public function getEndDate(): ?\DateTimeImmutable | ||||
|     { | ||||
|         return $this->endDate; | ||||
|     } | ||||
|  | ||||
|     public function setEndDate(?\DateTimeImmutable $endDate): void | ||||
|     { | ||||
|         $this->endDate = $endDate; | ||||
|     } | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
| } | ||||
| @@ -549,7 +549,7 @@ class User implements UserInterface, \Stringable | ||||
|         $this->scopeHistories[] = $newScope; | ||||
|  | ||||
|         $criteria = new Criteria(); | ||||
|         $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); | ||||
|         $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); | ||||
|  | ||||
|         /** @var \Iterator $scopes */ | ||||
|         $scopes = $this->scopeHistories->matching($criteria)->getIterator(); | ||||
| @@ -605,7 +605,7 @@ class User implements UserInterface, \Stringable | ||||
|         $this->jobHistories[] = $newJob; | ||||
|  | ||||
|         $criteria = new Criteria(); | ||||
|         $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); | ||||
|         $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); | ||||
|  | ||||
|         /** @var \Iterator $jobs */ | ||||
|         $jobs = $this->jobHistories->matching($criteria)->getIterator(); | ||||
|   | ||||
| @@ -1,56 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Entity\NewsItem; | ||||
| use Chill\MainBundle\Form\Type\ChillDateType; | ||||
| use Chill\MainBundle\Form\Type\ChillTextareaType; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
|  | ||||
| class NewsItemType extends AbstractType | ||||
| { | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         $builder | ||||
|             ->add('title', TextType::class, [ | ||||
|                 'required' => true, | ||||
|             ]) | ||||
|             ->add('content', ChillTextareaType::class, [ | ||||
|                 'required' => false, | ||||
|             ]) | ||||
|             ->add( | ||||
|                 'startDate', | ||||
|                 ChillDateType::class, | ||||
|                 [ | ||||
|                     'required' => true, | ||||
|                     'input' => 'datetime_immutable', | ||||
|                     'label' => 'news.startDate', | ||||
|                 ] | ||||
|             ) | ||||
|             ->add('endDate', ChillDateType::class, [ | ||||
|                 'required' => false, | ||||
|                 'input' => 'datetime_immutable', | ||||
|                 'label' => 'news.endDate', | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return void | ||||
|      */ | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     { | ||||
|         $resolver->setDefault('data_class', NewsItem::class); | ||||
|     } | ||||
| } | ||||
| @@ -1,145 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\NewsItem; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
|  | ||||
| class NewsItemRepository implements ObjectRepository | ||||
| { | ||||
|     private readonly EntityRepository $repository; | ||||
|  | ||||
|     public function __construct(EntityManagerInterface $entityManager, private readonly ClockInterface $clock) | ||||
|     { | ||||
|         $this->repository = $entityManager->getRepository(NewsItem::class); | ||||
|     } | ||||
|  | ||||
|     public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder | ||||
|     { | ||||
|         return $this->repository->createQueryBuilder($alias, $indexBy); | ||||
|     } | ||||
|  | ||||
|     public function find($id) | ||||
|     { | ||||
|         return $this->repository->find($id); | ||||
|     } | ||||
|  | ||||
|     public function findAll() | ||||
|     { | ||||
|         return $this->repository->findAll(); | ||||
|     } | ||||
|  | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|  | ||||
|     public function findOneBy(array $criteria) | ||||
|     { | ||||
|         return $this->repository->findOneBy($criteria); | ||||
|     } | ||||
|  | ||||
|     public function getClassName() | ||||
|     { | ||||
|         return NewsItem::class; | ||||
|     } | ||||
|  | ||||
|     private function buildBaseQuery( | ||||
|         ?string $pattern = null | ||||
|     ): QueryBuilder { | ||||
|         $qb = $this->createQueryBuilder('n'); | ||||
|  | ||||
|         $qb->where('n.startDate <= :now'); | ||||
|         $qb->setParameter('now', $this->clock->now()); | ||||
|  | ||||
|         if (null !== $pattern && '' !== $pattern) { | ||||
|             $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))')) | ||||
|                 ->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))')) | ||||
|                 ->setParameter('pattern', '%'.$pattern.'%'); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function findAllFilteredBySearchTerm(?string $pattern = null) | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($pattern); | ||||
|         $qb | ||||
|             ->addOrderBy('n.startDate', 'DESC') | ||||
|             ->addOrderBy('n.id', 'DESC'); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list<NewsItem> | ||||
|      */ | ||||
|     public function findCurrentNews(?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         $qb = $this->buildQueryCurrentNews(); | ||||
|         $qb->addOrderBy('n.startDate', 'DESC'); | ||||
|  | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         if (null !== $offset) { | ||||
|             $qb->setFirstResult($offset); | ||||
|         } | ||||
|  | ||||
|         return $qb | ||||
|             ->getQuery() | ||||
|             ->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function countAllFilteredBySearchTerm(?string $pattern = null) | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($pattern); | ||||
|  | ||||
|         return $qb | ||||
|             ->select('COUNT(n)') | ||||
|             ->getQuery() | ||||
|             ->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     public function countCurrentNews() | ||||
|     { | ||||
|         return $this->buildQueryCurrentNews() | ||||
|             ->select('COUNT(n)') | ||||
|             ->getQuery() | ||||
|             ->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     private function buildQueryCurrentNews(): QueryBuilder | ||||
|     { | ||||
|         $now = $this->clock->now(); | ||||
|  | ||||
|         $qb = $this->createQueryBuilder('n'); | ||||
|         $qb | ||||
|             ->where( | ||||
|                 $qb->expr()->andX( | ||||
|                     $qb->expr()->lte('n.startDate', ':now'), | ||||
|                     $qb->expr()->orX( | ||||
|                         $qb->expr()->gt('n.endDate', ':now'), | ||||
|                         $qb->expr()->isNull('n.endDate') | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|             ->setParameter('now', $now); | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
| } | ||||
| @@ -12,7 +12,6 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| @@ -21,7 +20,7 @@ final class ScopeRepository implements ScopeRepositoryInterface | ||||
| { | ||||
|     private readonly EntityRepository $repository; | ||||
|  | ||||
|     public function __construct(EntityManagerInterface $entityManager, private readonly TranslatableStringHelperInterface $translatableStringHelper) | ||||
|     public function __construct(EntityManagerInterface $entityManager) | ||||
|     { | ||||
|         $this->repository = $entityManager->getRepository(Scope::class); | ||||
|     } | ||||
| @@ -46,11 +45,11 @@ final class ScopeRepository implements ScopeRepositoryInterface | ||||
|  | ||||
|     public function findAllActive(): array | ||||
|     { | ||||
|         $scopes = $this->repository->findBy(['active' => true]); | ||||
|         $qb = $this->repository->createQueryBuilder('s'); | ||||
|  | ||||
|         usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); | ||||
|         $qb->where('s.active = \'TRUE\''); | ||||
|  | ||||
|         return $scopes; | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -40,11 +40,7 @@ readonly class UserJobRepository implements UserJobRepositoryInterface | ||||
|  | ||||
|     public function findAllActive(): array | ||||
|     { | ||||
|         $jobs = $this->repository->findBy(['active' => true]); | ||||
|  | ||||
|         usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); | ||||
|  | ||||
|         return $jobs; | ||||
|         return $this->repository->findBy(['active' => true]); | ||||
|     } | ||||
|  | ||||
|     public function findAllOrderedByName(): array | ||||
|   | ||||
| @@ -106,8 +106,8 @@ final readonly class UserRepository implements UserRepositoryInterface | ||||
|             FROM users u | ||||
|             LEFT JOIN chill_main_civility civility ON u.civility_id = civility.id | ||||
|             LEFT JOIN centers mainCenter ON u.maincenter_id = mainCenter.id | ||||
|             LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() | ||||
|             LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id | ||||
|             LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id | ||||
|             LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() | ||||
|             LEFT JOIN chill_main_user_scope_history userScopeHistory ON u.id = userScopeHistory.user_id AND tstzrange(userScopeHistory.startdate, userScopeHistory.enddate) @> NOW() | ||||
|             LEFT JOIN scopes mainScope ON userScopeHistory.scope_id = mainScope.id | ||||
|             LEFT JOIN chill_main_location currentLocation ON u.currentlocation_id = currentLocation.id | ||||
|   | ||||
| @@ -2,14 +2,14 @@ import AddressDetailsButton from "../../vuejs/_components/AddressDetails/Address | ||||
| import {createApp} from "vue"; | ||||
| import {createI18n} from "vue-i18n"; | ||||
| import {_createI18n} from "../../vuejs/_js/i18n"; | ||||
| import {Address} from "../../types"; | ||||
| import {Address, AddressRefStatus} from "../../types"; | ||||
|  | ||||
| const i18n = _createI18n({}); | ||||
|  | ||||
| document.querySelectorAll<HTMLSpanElement>('span[data-address-details]').forEach((el) => { | ||||
|   const dataset = el.dataset as { | ||||
|     addressId: string, | ||||
|     addressRefStatus: string, | ||||
|     addressRefStatus: AddressRefStatus, | ||||
|   }; | ||||
|  | ||||
|   const app = createApp({ | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| import './index.scss'; | ||||
| @@ -1,7 +0,0 @@ | ||||
| div.flex-table { | ||||
|     .news-content { | ||||
|         p { | ||||
|             margin-top: 1rem; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -160,11 +160,3 @@ export interface LocationType { | ||||
|   contactData: "optional" | "required"; | ||||
|   title: TranslatableString; | ||||
| } | ||||
|  | ||||
| export interface NewsItemType { | ||||
|     id: number; | ||||
|     title: string; | ||||
|     content: string; | ||||
|     startDate: DateTime; | ||||
|     endDate: DateTime | null; | ||||
| } | ||||
|   | ||||
| @@ -97,8 +97,6 @@ import MyNotifications from './MyNotifications'; | ||||
| import MyWorkflows from './MyWorkflows.vue'; | ||||
| import TabCounter from './TabCounter'; | ||||
| import { mapState } from "vuex"; | ||||
| import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; | ||||
|  | ||||
|  | ||||
| export default { | ||||
|    name: "App", | ||||
| @@ -114,7 +112,7 @@ export default { | ||||
|    }, | ||||
|    data() { | ||||
|       return { | ||||
|          activeTab: 'MyCustoms', | ||||
|          activeTab: 'MyCustoms' | ||||
|       } | ||||
|    }, | ||||
|    computed: { | ||||
| @@ -128,11 +126,8 @@ export default { | ||||
|    }, | ||||
|    methods: { | ||||
|       selectTab(tab) { | ||||
|           if (tab !== 'MyCustoms') { | ||||
|               this.$store.dispatch('getByTab', { tab: tab }); | ||||
|           } | ||||
|          this.$store.dispatch('getByTab', { tab: tab }); | ||||
|          this.activeTab = tab; | ||||
|           console.log(this.activeTab) | ||||
|       } | ||||
|    }, | ||||
|    mounted() { | ||||
|   | ||||
| @@ -1,47 +0,0 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <h1>{{ $t('widget.news.title') }}</h1> | ||||
|         <ul v-if="newsItems.length > 0" class="scrollable"> | ||||
|             <NewsItem v-for="item in newsItems" :item="item" :key="item.id" /> | ||||
|         </ul> | ||||
|         <p v-if="newsItems.length === 0 " class="chill-no-data-statement">{{ $t('widget.news.none') }}</p> | ||||
|     </div> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, ref } from 'vue' | ||||
| import { fetchResults } from '../../../lib/api/apiMethods'; | ||||
| import Modal from '../../_components/Modal.vue'; | ||||
| import { NewsItemType } from '../../../types'; | ||||
| import NewsItem from './NewsItem.vue'; | ||||
|  | ||||
| const newsItems = ref<NewsItemType[]>([]) | ||||
|  | ||||
| onMounted(() => { | ||||
|     fetchResults<NewsItemType>('/api/1.0/main/news/current.json') | ||||
|         .then((news): Promise<void> => { | ||||
|             // console.log('news articles', response.results) | ||||
|             newsItems.value = news; | ||||
|  | ||||
|             return Promise.resolve(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|             console.error('Error fetching news items', error); | ||||
|         }) | ||||
| }) | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| ul { | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
|  | ||||
| </style> | ||||
| @@ -1,183 +0,0 @@ | ||||
| <template> | ||||
|     <li> | ||||
|         <h2>{{ props.item.title }}</h2> | ||||
|         <time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time> | ||||
|         <div class="content" v-if="shouldTruncate(item.content)"> | ||||
|             <div v-html="prepareContent(item.content)"></div> | ||||
|             <div class="float-end"> | ||||
|                 <button class="btn btn-sm btn-show read-more" @click="() => openModal(item)">{{ $t('widget.news.readMore') }}</button> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="content" v-else> | ||||
|             <div v-html="convertMarkdownToHtml(item.content)"></div> | ||||
|         </div> | ||||
|  | ||||
|         <modal v-if="showModal" @close="closeModal"> | ||||
|             <template #header> | ||||
|                 <p class="news-title">{{ item.title }}</p> | ||||
|             </template> | ||||
|             <template #body> | ||||
|                 <p class="news-date"> | ||||
|                     <time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time> | ||||
|                 </p> | ||||
|                 <div v-html="convertMarkdownToHtml(item.content)"></div> | ||||
|             </template> | ||||
|         </modal> | ||||
|     </li> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; | ||||
| import { marked } from 'marked'; | ||||
| import DOMPurify from 'dompurify'; | ||||
| import { DateTime, NewsItemType } from "../../../types"; | ||||
| import type { PropType } from 'vue' | ||||
| import { ref } from "vue"; | ||||
| import {ISOToDatetime} from '../../../chill/js/date'; | ||||
|  | ||||
|  | ||||
| const props = defineProps({ | ||||
|     item: { | ||||
|         type: Object as PropType<NewsItemType>, | ||||
|         required: true | ||||
|     }, | ||||
|     maxLength: { | ||||
|         type: Number, | ||||
|         required: false, | ||||
|         default: 350, | ||||
|     }, | ||||
|     maxLines: { | ||||
|         type: Number, | ||||
|         required: false, | ||||
|         default: 3 | ||||
|     } | ||||
| }) | ||||
|  | ||||
| const selectedArticle = ref<NewsItemType | null>(null); | ||||
| const showModal = ref(false); | ||||
|  | ||||
| const openModal = (item: NewsItemType) => { | ||||
|     selectedArticle.value = item; | ||||
|     showModal.value = true; | ||||
| }; | ||||
|  | ||||
| const closeModal = () => { | ||||
|     selectedArticle.value = null; | ||||
|     showModal.value = false; | ||||
| }; | ||||
|  | ||||
| const shouldTruncate = (content: string): boolean => { | ||||
|     const lines = content.split('\n'); | ||||
|  | ||||
|     // Check if any line exceeds the maximum length | ||||
|     const tooManyLines = lines.length > props.maxLines; | ||||
|  | ||||
|     return content.length > props.maxLength || tooManyLines; | ||||
| }; | ||||
|  | ||||
| const truncateContent = (content: string): string => { | ||||
|     let truncatedContent = content.slice(0, props.maxLength); | ||||
|     let linkDepth = 0; | ||||
|     let linkStartIndex = -1; | ||||
|     const lines = content.split('\n'); | ||||
|  | ||||
|     // Truncate if amount of lines are too many | ||||
|     if (lines.length > props.maxLines && content.length < props.maxLength) { | ||||
|         const truncatedContent = lines.slice(0, props.maxLines).join('\n').trim(); | ||||
|         return truncatedContent + '...'; | ||||
|     } | ||||
|  | ||||
|     for (let i = 0; i < truncatedContent.length; i++) { | ||||
|         const char = truncatedContent[i]; | ||||
|  | ||||
|         if (char === '[') { | ||||
|             linkDepth++; | ||||
|             if (linkDepth === 1) { | ||||
|                 linkStartIndex = i; | ||||
|             } | ||||
|         } else if (char === ']') { | ||||
|             linkDepth = Math.max(0, linkDepth - 1); | ||||
|         } else if (char === '(' && linkDepth === 0) { | ||||
|             truncatedContent = truncatedContent.slice(0, i); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     while (linkDepth > 0) { | ||||
|         truncatedContent += ']'; | ||||
|         linkDepth--; | ||||
|     } | ||||
|  | ||||
|     // If a link was found, append the URL inside the parentheses | ||||
|     if (linkStartIndex !== -1) { | ||||
|         const linkEndIndex = content.indexOf(')', linkStartIndex); | ||||
|         const url = content.slice(linkStartIndex + 1, linkEndIndex); | ||||
|         truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`; | ||||
|     } | ||||
|  | ||||
|     truncatedContent += '...'; | ||||
|  | ||||
|     return truncatedContent; | ||||
| }; | ||||
|  | ||||
| const preprocess = (markdown: string): string => { | ||||
|     return markdown; | ||||
| } | ||||
|  | ||||
| const postprocess = (html: string): string => { | ||||
|     DOMPurify.addHook('afterSanitizeAttributes', (node) => { | ||||
|         if ('target' in node) { | ||||
|             node.setAttribute('target', '_blank'); | ||||
|             node.setAttribute('rel', 'noopener noreferrer'); | ||||
|         } | ||||
|         if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) { | ||||
|             node.setAttribute('xlink:show', 'new'); | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     return DOMPurify.sanitize(html); | ||||
| } | ||||
|  | ||||
| const convertMarkdownToHtml = (markdown: string): string => { | ||||
|     marked.use({'hooks': {postprocess, preprocess}}); | ||||
|     const rawHtml = marked(markdown); | ||||
|     return rawHtml; | ||||
| }; | ||||
|  | ||||
| const prepareContent = (content: string): string => { | ||||
|     const htmlContent = convertMarkdownToHtml(content); | ||||
|     return truncateContent(htmlContent); | ||||
| }; | ||||
|  | ||||
| const newsItemStartDate = (): null|Date => { | ||||
|     return ISOToDatetime(props.item?.startDate.datetime); | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|  | ||||
| li { | ||||
|     margin-bottom: 20px; | ||||
|     overflow: hidden; | ||||
|     padding: .8rem; | ||||
|     background-color: #fbfbfb; | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|     font-size: 1rem !important; | ||||
|     text-transform: uppercase; | ||||
| } | ||||
|  | ||||
| .content { | ||||
|     overflow: hidden; | ||||
|     font-size: .9rem; | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .news-title { | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| </style> | ||||
| @@ -1,73 +1,76 @@ | ||||
| <template> | ||||
|    <span v-if="noResults" class="chill-no-data-statement">{{ $t('no_dashboard') }}</span> | ||||
|    <div v-else id="dashboards" class="container g-3"> | ||||
|        <div class="row"> | ||||
|            <div class="mbloc col-xs-12 col-sm-4"> | ||||
|                <div class="custom1"> | ||||
|                    <ul class="list-unstyled"> | ||||
|                        <li v-if="counter.notifications > 0"> | ||||
|                            <i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications"> | ||||
|                                <template v-slot:n><span>{{ counter.notifications }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                        <li v-if="counter.accompanyingCourses > 0"> | ||||
|                            <i18n-t keypath="counter.assignated_courses" tag="span" :class="counterClass" :plural="counter.accompanyingCourses"> | ||||
|                                <template v-slot:n><span>{{ counter.accompanyingCourses }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                        <li v-if="counter.works > 0"> | ||||
|                            <i18n-t keypath="counter.assignated_actions" tag="span" :class="counterClass" :plural="counter.works"> | ||||
|                                <template v-slot:n><span>{{ counter.works }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                        <li v-if="counter.evaluations > 0"> | ||||
|                            <i18n-t keypath="counter.assignated_evaluations" tag="span" :class="counterClass" :plural="counter.evaluations"> | ||||
|                                <template v-slot:n><span>{{ counter.evaluations }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                        <li v-if="counter.tasksAlert > 0"> | ||||
|                            <i18n-t keypath="counter.alert_tasks" tag="span" :class="counterClass" :plural="counter.tasksAlert"> | ||||
|                                <template v-slot:n><span>{{ counter.tasksAlert }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                        <li v-if="counter.tasksWarning > 0"> | ||||
|                            <i18n-t keypath="counter.warning_tasks" tag="span" :class="counterClass" :plural="counter.tasksWarning"> | ||||
|                                <template v-slot:n><span>{{ counter.tasksWarning }}</span></template> | ||||
|                            </i18n-t> | ||||
|                        </li> | ||||
|                    </ul> | ||||
|                </div> | ||||
|            </div> | ||||
|    <div v-else id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'> | ||||
|        | ||||
|       <div class="mbloc col col-sm-6 col-lg-4"> | ||||
|          <div class="custom1"> | ||||
|             <ul class="list-unstyled"> | ||||
|                <li v-if="counter.notifications > 0"> | ||||
|                   <i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications"> | ||||
|                      <template v-slot:n><span>{{ counter.notifications }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|                <li v-if="counter.accompanyingCourses > 0"> | ||||
|                   <i18n-t keypath="counter.assignated_courses" tag="span" :class="counterClass" :plural="counter.accompanyingCourses"> | ||||
|                      <template v-slot:n><span>{{ counter.accompanyingCourses }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|                <li v-if="counter.works > 0"> | ||||
|                   <i18n-t keypath="counter.assignated_actions" tag="span" :class="counterClass" :plural="counter.works"> | ||||
|                      <template v-slot:n><span>{{ counter.works }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|                <li v-if="counter.evaluations > 0"> | ||||
|                   <i18n-t keypath="counter.assignated_evaluations" tag="span" :class="counterClass" :plural="counter.evaluations"> | ||||
|                      <template v-slot:n><span>{{ counter.evaluations }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|                <li v-if="counter.tasksAlert > 0"> | ||||
|                   <i18n-t keypath="counter.alert_tasks" tag="span" :class="counterClass" :plural="counter.tasksAlert"> | ||||
|                      <template v-slot:n><span>{{ counter.tasksAlert }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|                <li v-if="counter.tasksWarning > 0"> | ||||
|                   <i18n-t keypath="counter.warning_tasks" tag="span" :class="counterClass" :plural="counter.tasksWarning"> | ||||
|                      <template v-slot:n><span>{{ counter.tasksWarning }}</span></template> | ||||
|                   </i18n-t> | ||||
|                </li> | ||||
|             </ul> | ||||
|          </div> | ||||
|       </div> | ||||
|        | ||||
|       <!-- | ||||
|       <div class="mbloc col col-sm-6 col-lg-4"> | ||||
|          <div class="custom2"> | ||||
|             Mon dashboard personnalisé | ||||
|          </div> | ||||
|       </div> | ||||
|       <div class="mbloc col col-sm-6 col-lg-4"> | ||||
|          <div class="custom3"> | ||||
|             Mon dashboard personnalisé | ||||
|          </div> | ||||
|       </div> | ||||
|       <div class="mbloc col col-sm-6 col-lg-4"> | ||||
|          <div class="custom4"> | ||||
|             Mon dashboard personnalisé | ||||
|          </div> | ||||
|       </div> | ||||
|       --> | ||||
|  | ||||
|            <template v-if="this.hasDashboardItems"> | ||||
|                <template v-for="dashboardItem in this.dashboardItems"> | ||||
|                    <div class="mbloc col-xs-12 col-sm-8 news" v-if="dashboardItem.type === 'news'"> | ||||
|                        <News /> | ||||
|                    </div> | ||||
|                </template> | ||||
|            </template> | ||||
|  | ||||
|        </div> | ||||
|    </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from "vuex"; | ||||
| import {makeFetch} from "ChillMainAssets/lib/api/apiMethods"; | ||||
| import News from './DashboardWidgets/News.vue'; | ||||
| import Masonry from 'masonry-layout/masonry'; | ||||
|  | ||||
| export default { | ||||
|    name: "MyCustoms", | ||||
|     components: { | ||||
|        News | ||||
|     }, | ||||
|    data() { | ||||
|       return { | ||||
|          counterClass: { | ||||
|             counter: true   //hack to pass class 'counter' in i18n-t | ||||
|          }, | ||||
|          dashboardItems: [], | ||||
|          masonry: null, | ||||
|          } | ||||
|       } | ||||
|    }, | ||||
|    computed: { | ||||
| @@ -75,19 +78,11 @@ export default { | ||||
|       noResults() { | ||||
|          return false | ||||
|       }, | ||||
|        hasDashboardItems() { | ||||
|            return this.dashboardItems.length > 0; | ||||
|        } | ||||
|    }, | ||||
|    mounted() { | ||||
|        makeFetch('GET', '/api/1.0/main/dashboard-config-item.json') | ||||
|            .then((response) => { | ||||
|                this.dashboardItems = response; | ||||
|            }) | ||||
|            .catch((error) => { | ||||
|                throw error | ||||
|            }); | ||||
|    }, | ||||
|       const elem = document.querySelector('#dashboards'); | ||||
|       const masonry = new Masonry(elem, {}); | ||||
|    } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @@ -103,10 +98,4 @@ span.counter { | ||||
|       background-color: unset; | ||||
|    } | ||||
| } | ||||
|  | ||||
| div.news { | ||||
|     max-height: 22rem; | ||||
|     overflow: hidden; | ||||
|     overflow-y: scroll; | ||||
| } | ||||
| </style> | ||||
| </style> | ||||
| @@ -63,15 +63,7 @@ const appMessages = { | ||||
|         }, | ||||
|         emergency: "Urgent", | ||||
|         confidential: "Confidentiel", | ||||
|         automatic_notification: "Notification automatique", | ||||
|         widget: { | ||||
|           news: { | ||||
|             title: "Actualités", | ||||
|             readMore: "Lire la suite", | ||||
|             date: "Date", | ||||
|             none: "Aucune actualité" | ||||
|           } | ||||
|         } | ||||
|         automatic_notification: "Notification automatique" | ||||
|     } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -96,11 +96,13 @@ const store = createStore({ | ||||
|         }, | ||||
|         catchError(state, error) { | ||||
|             state.errorMsg.push(error); | ||||
|         }, | ||||
|         } | ||||
|     }, | ||||
|     actions: { | ||||
|         getByTab({ commit, getters }, { tab, param }) { | ||||
|             switch (tab) { | ||||
|                 case 'MyCustoms': | ||||
|                     break; | ||||
|                 // case 'MyWorks': | ||||
|                 //     if (!getters.isWorksLoaded) { | ||||
|                 //         commit('setLoading', true); | ||||
| @@ -219,8 +221,8 @@ const store = createStore({ | ||||
|                 default: | ||||
|                     throw 'tab '+ tab; | ||||
|             } | ||||
|         }, | ||||
|         } | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| export { store }; | ||||
| export { store }; | ||||
| @@ -18,17 +18,15 @@ export interface AddressModalContentProps { | ||||
|   address_ref_status: AddressRefStatus; | ||||
| } | ||||
|  | ||||
| interface AddressModalData { | ||||
|     loading: boolean, | ||||
|     working_address: Address | null, | ||||
|     working_ref_status: AddressRefStatus | null, | ||||
| } | ||||
|  | ||||
| const data: AddressModalData = reactive({ | ||||
| const data = reactive<{ | ||||
|   loading: boolean, | ||||
|   working_address: Address | null, | ||||
|   working_ref_status: AddressRefStatus | null, | ||||
| }>({ | ||||
|   loading: false, | ||||
|   working_address: null, | ||||
|   working_ref_status: null, | ||||
| } as AddressModalData); | ||||
| }); | ||||
|  | ||||
| const props = defineProps<AddressModalContentProps>(); | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,7 @@ const messages = { | ||||
|          years_old: "1 an | {n} an | {n} ans", | ||||
|          residential_address: "Adresse de résidence", | ||||
|          located_at: "réside chez" | ||||
|       }, | ||||
|       } | ||||
|    } | ||||
| }; | ||||
|  | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user