mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'origin/master' into upgrade-sf5
This commit is contained in:
commit
931c69eee4
3
.changes/v2.16.2.md
Normal file
3
.changes/v2.16.2.md
Normal file
@ -0,0 +1,3 @@
|
||||
## v2.16.2 - 2024-02-21
|
||||
### Fixed
|
||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
5
.changes/v2.16.3.md
Normal file
5
.changes/v2.16.3.md
Normal file
@ -0,0 +1,5 @@
|
||||
## v2.16.3 - 2024-02-26
|
||||
### Fixed
|
||||
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||
### UX
|
||||
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
9
.changes/v2.17.0.md
Normal file
9
.changes/v2.17.0.md
Normal file
@ -0,0 +1,9 @@
|
||||
## v2.17.0 - 2024-03-19
|
||||
### Feature
|
||||
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
|
||||
### Fixed
|
||||
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
5
.changes/v2.18.0.md
Normal file
5
.changes/v2.18.0.md
Normal file
@ -0,0 +1,5 @@
|
||||
## v2.18.0 - 2024-03-26
|
||||
### Feature
|
||||
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||
### Fixed
|
||||
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
3
.changes/v2.18.1.md
Normal file
3
.changes/v2.18.1.md
Normal file
@ -0,0 +1,3 @@
|
||||
## v2.18.1 - 2024-03-26
|
||||
### Fixed
|
||||
* Fix layout issue in document generation for admin (minor)
|
@ -23,3 +23,7 @@ max_line_length = 0
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[.rst]
|
||||
ident_size = 3
|
||||
ident_style = space
|
||||
|
||||
|
@ -91,7 +91,7 @@ rector_tests:
|
||||
stage: Tests
|
||||
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
|
||||
script:
|
||||
- tests/console cache:clear --env=test
|
||||
- tests/console cache:clear
|
||||
- bin/rector process --dry-run
|
||||
cache:
|
||||
paths:
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -6,6 +6,36 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## 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,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati
|
||||
Continue to explain the export framework
|
||||
|
||||
.. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main
|
||||
|
||||
|
||||
With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ?
|
||||
``````````````````````````````````````````````````````````````````````````````````````````````````````
|
||||
|
||||
As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course
|
||||
which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on
|
||||
two accompanying period only: the ones with id 329 and 334.
|
||||
|
||||
Let's see the list of activities associated with those accompanying period:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7
|
||||
ORDER BY accompanyingperiod_id;
|
||||
|
||||
We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one.
|
||||
|
||||
.. csv-table::
|
||||
:header: id, accompanyingperiod_id, type_id
|
||||
|
||||
990,329,7
|
||||
986,329,7
|
||||
987,329,7
|
||||
993,329,7
|
||||
991,329,7
|
||||
992,329,7
|
||||
1000,334,7
|
||||
|
||||
Let's calculate the average duration for those accompanying periods, and the number of period:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334);
|
||||
|
||||
The result of this query is:
|
||||
|
||||
.. csv-table::
|
||||
:header: AVG, COUNT
|
||||
|
||||
2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2
|
||||
|
||||
Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep
|
||||
only the accompanying period which contains the given activity type:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
What are the results here ?
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
7
|
||||
|
||||
:code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which
|
||||
are taken into account for the computation:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
.. csv-table::
|
||||
:header: accompanyingperiod.id, activity.id
|
||||
|
||||
329,993
|
||||
334,1000
|
||||
329,987
|
||||
329,990
|
||||
329,991
|
||||
329,992
|
||||
329,986
|
||||
|
||||
For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we
|
||||
count one entry for each activity (actually, we count the number of activities).
|
||||
|
||||
So, let's use the :code:`DISTINCT` keyword to count only once the equal ids:
|
||||
|
||||
.. code-block::
|
||||
|
||||
SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
Now, it works again...
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
2
|
||||
|
||||
But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and
|
||||
:code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result.
|
||||
|
||||
The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id);
|
||||
|
||||
The result is correct without :code:`DISTINCT` keyword:
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
2
|
||||
|
||||
And we can now compute the average duration without fear:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id);
|
||||
|
||||
Give the result:
|
||||
|
||||
.. csv-table::
|
||||
:header: AVG
|
||||
|
||||
2 years 2 mons 21 days 12 hours 0 mins 0.0 secs
|
||||
|
@ -79,7 +79,7 @@ final readonly class CreatorJobFilter implements FilterInterface
|
||||
{
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'choices' => $this->userJobRepository->findAllOrderedByName(),
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'class' => UserJob::class,
|
||||
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
|
||||
$s->getLabel()
|
||||
|
@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface
|
||||
private const PREFIX = 'acp_act_filter_creator_scope';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelper $translatableStringHelper
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private readonly ScopeRepositoryInterface $scopeRepository,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
@ -75,6 +77,7 @@ class CreatorScopeFilter implements FilterInterface
|
||||
$builder
|
||||
->add('scopes', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'choices' => $this->scopeRepository->findAllActive(),
|
||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
),
|
||||
|
@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -27,8 +28,10 @@ class UsersJobFilter implements FilterInterface
|
||||
private const PREFIX = 'act_filter_user_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {}
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
@ -68,6 +71,7 @@ class UsersJobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
|
@ -95,7 +95,7 @@ class ActivityType extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */
|
||||
/** @var AccompanyingPeriod|null $accompanyingPeriod */
|
||||
$accompanyingPeriod = null;
|
||||
|
||||
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
||||
|
@ -242,7 +242,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
||||
thirdparties.thirdpartyids,
|
||||
persons.personids,
|
||||
actions.socialactionids,
|
||||
issues.socialissueids
|
||||
issues.socialissueids,
|
||||
a.user_id
|
||||
|
||||
FROM activity a
|
||||
LEFT JOIN chill_main_location location ON a.location_id = location.id
|
||||
@ -282,6 +283,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
||||
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
|
||||
->addFieldResult('activityPresence', 'presence_id', 'id')
|
||||
->addFieldResult('activityPresence', 'presence_name', 'name')
|
||||
->addScalarResult('user_id', 'userId', Types::INTEGER)
|
||||
|
||||
// results which cannot be mapped into entity
|
||||
->addScalarResult('comment_comment', 'comment', Types::TEXT)
|
||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\ActivityBundle\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
||||
@ -111,7 +112,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list
|
||||
* @return list<Activity>
|
||||
*/
|
||||
private function filterActivitiesByUser(array $activities, User $user): array
|
||||
{
|
||||
@ -119,6 +120,12 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
array_filter(
|
||||
$activities,
|
||||
function ($activity) use ($user) {
|
||||
$u = $activity['user'];
|
||||
|
||||
if (null !== $u && $u['username'] === $user->getUsername()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []);
|
||||
|
||||
return \in_array($user->getUsername(), $activityUsernames, true);
|
||||
@ -128,7 +135,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list
|
||||
* @return list<AccompanyingPeriod\AccompanyingPeriodWork>
|
||||
*/
|
||||
private function filterWorksByUser(array $works, User $user): array
|
||||
{
|
||||
@ -215,6 +222,15 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
foreach ($activities as $row) {
|
||||
$activity = $row[0];
|
||||
|
||||
$user = match (null === $row['userId']) {
|
||||
false => $this->userRepository->find($row['userId']),
|
||||
true => null,
|
||||
};
|
||||
|
||||
$activity['user'] = $this->normalizer->normalize($user, 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class,
|
||||
]);
|
||||
|
||||
$activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class,
|
||||
]);
|
||||
|
@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
||||
self::assertIsArray($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||
*/
|
||||
public function testfindByAccompanyingPeriodSimplified(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void
|
||||
{
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted($role, $period)->willReturn(true);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
$repository = new ActivityACLAwareRepository(
|
||||
$this->authorizationHelperForCurrentUser,
|
||||
$this->centerResolverManager,
|
||||
$this->activityRepository,
|
||||
$this->entityManager,
|
||||
$security->reveal(),
|
||||
$this->requestStack
|
||||
);
|
||||
|
||||
$actual = $repository->findByAccompanyingPeriodSimplified($period);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||
*/
|
||||
@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
||||
->getQuery()
|
||||
->getResult()
|
||||
) {
|
||||
throw new \RuntimeException('no jobs found');
|
||||
$job = new UserJob();
|
||||
$job->setLabel(['fr' => 'test']);
|
||||
$this->entityManager->persist($job);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
if (null === $user = $this->entityManager
|
||||
|
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Tests\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Service\DocGenerator\ListActivitiesByAccompanyingPeriodContext;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ListActivitiesByAccompanyingPeriodContextTest extends KernelTestCase
|
||||
{
|
||||
private ListActivitiesByAccompanyingPeriodContext $listActivitiesByAccompanyingPeriodContext;
|
||||
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class);
|
||||
$this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class);
|
||||
$this->userRepository = self::$container->get(UserRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAccompanyingPeriod
|
||||
*/
|
||||
public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void
|
||||
{
|
||||
$context = $this->getContext();
|
||||
$template = new DocGeneratorTemplate();
|
||||
$template->setOptions([
|
||||
'mainPerson' => false,
|
||||
'person1' => false,
|
||||
'person2' => false,
|
||||
'thirdParty' => false,
|
||||
]);
|
||||
|
||||
$data = $context->getData(
|
||||
$template,
|
||||
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||
['myActivitiesOnly' => false, 'myWorksOnly' => false]
|
||||
);
|
||||
|
||||
self::assertIsArray($data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertIsArray($data['activities']);
|
||||
self::assertGreaterThan(0, count($data['activities']));
|
||||
self::assertIsArray($data['activities'][0]);
|
||||
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||
self::assertIsArray($data['activities'][0]['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAccompanyingPeriod
|
||||
*/
|
||||
public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void
|
||||
{
|
||||
$context = $this->getContext();
|
||||
$template = new DocGeneratorTemplate();
|
||||
$template->setOptions([
|
||||
'mainPerson' => false,
|
||||
'person1' => false,
|
||||
'person2' => false,
|
||||
'thirdParty' => false,
|
||||
]);
|
||||
|
||||
$data = $context->getData(
|
||||
$template,
|
||||
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||
['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)]
|
||||
);
|
||||
|
||||
self::assertIsArray($data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertIsArray($data['activities']);
|
||||
self::assertGreaterThan(0, count($data['activities']));
|
||||
self::assertIsArray($data['activities'][0]);
|
||||
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||
self::assertIsArray($data['activities'][0]['user']);
|
||||
}
|
||||
|
||||
public static function provideAccompanyingPeriod(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult()) {
|
||||
throw new \RuntimeException('no period found');
|
||||
}
|
||||
|
||||
if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult()
|
||||
) {
|
||||
throw new \RuntimeException('no user found');
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity
|
||||
->setAccompanyingPeriod($period)
|
||||
->setUser($user)
|
||||
->setDate(new \DateTime());
|
||||
|
||||
$em->persist($activity);
|
||||
$em->flush();
|
||||
|
||||
self::ensureKernelShutdown();
|
||||
|
||||
return [
|
||||
[$period->getId(), $user->getId()],
|
||||
];
|
||||
}
|
||||
|
||||
private function getContext(): ListActivitiesByAccompanyingPeriodContext
|
||||
{
|
||||
return $this->listActivitiesByAccompanyingPeriodContext;
|
||||
}
|
||||
}
|
@ -396,7 +396,7 @@ export:
|
||||
by_creator_job:
|
||||
job_form_label: Métiers
|
||||
Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange
|
||||
'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%"
|
||||
'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%"
|
||||
by_persons:
|
||||
Filter activity by persons: Filtrer les échanges par usager participant
|
||||
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'
|
||||
|
@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface
|
||||
private const PREFIX = 'aside_act_filter_user_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
@ -68,6 +70,7 @@ class ByUserJobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
|
@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -26,8 +27,10 @@ final readonly class JobFilter implements FilterInterface
|
||||
private const PREFIX = 'cal_filter_job';
|
||||
|
||||
public function __construct(
|
||||
private TranslatableStringHelper $translatableStringHelper
|
||||
) {}
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private UserJobRepositoryInterface $userJobRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
@ -73,6 +76,7 @@ final readonly class JobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('job', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize(
|
||||
$j->getLabel()
|
||||
),
|
||||
|
@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -28,8 +29,10 @@ class ScopeFilter implements FilterInterface
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
private readonly TranslatableStringHelper $translatableStringHelper
|
||||
) {}
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private readonly ScopeRepositoryInterface $scopeRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
@ -75,6 +78,7 @@ class ScopeFilter implements FilterInterface
|
||||
$builder
|
||||
->add('scope', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'choices' => $this->scopeRepository->findAllActive(),
|
||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
),
|
||||
|
@ -32,7 +32,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|null
|
||||
public function isUserAbsent(User $user): ?bool
|
||||
{
|
||||
$id = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
|
@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface
|
||||
/**
|
||||
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft
|
||||
*/
|
||||
public function isUserAbsent(User $user): bool|null;
|
||||
public function isUserAbsent(User $user): ?bool;
|
||||
}
|
||||
|
@ -16,25 +16,30 @@ 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\Security\ChillSecurity;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
// TODO à mettre dans services
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
final class DocGeneratorTemplateController extends AbstractController
|
||||
{
|
||||
@ -45,6 +50,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly ChillSecurity $security
|
||||
) {}
|
||||
|
||||
@ -170,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
||||
}
|
||||
|
||||
$contextGenerationData = [
|
||||
'test_file' => null,
|
||||
];
|
||||
$contextGenerationData = [];
|
||||
|
||||
if (
|
||||
$context instanceof DocGeneratorContextWithPublicFormInterface
|
||||
@ -182,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
$builder = $this->createFormBuilder(
|
||||
array_merge(
|
||||
$context->getFormData($template, $entity),
|
||||
$isTest ? ['test_file' => null, 'show_data' => false] : []
|
||||
$isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : []
|
||||
)
|
||||
);
|
||||
|
||||
$context->buildPublicForm($builder, $template, $entity);
|
||||
} else {
|
||||
$builder = $this->createFormBuilder(
|
||||
['test_file' => null, 'show_data' => false]
|
||||
['creator' => null, 'show_data' => false, 'send_result_to' => '']
|
||||
);
|
||||
}
|
||||
|
||||
if ($isTest) {
|
||||
$builder->add('test_file', FileType::class, [
|
||||
'label' => 'Template file',
|
||||
$builder->add('dump_only', CheckboxType::class, [
|
||||
'label' => 'docgen.Show data instead of generating',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('show_data', CheckboxType::class, [
|
||||
'label' => 'Show data instead of generating',
|
||||
'required' => false,
|
||||
$builder->add('send_result_to', EmailType::class, [
|
||||
'label' => 'docgen.Send report to',
|
||||
'help' => 'docgen.Send report errors to this email address',
|
||||
'empty_data' => '',
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
new NotNull(),
|
||||
],
|
||||
]);
|
||||
$builder->add('creator', PickUserDynamicType::class, [
|
||||
'label' => 'docgen.Generate as creator',
|
||||
'help' => 'docgen.The document will be generated as the given creator',
|
||||
'multiple' => false,
|
||||
'constraints' => [
|
||||
new NotNull(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -211,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
||||
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
||||
$templateOptions = [
|
||||
'entity' => $entity, 'form' => $form->createView(),
|
||||
'template' => $template, 'context' => $context,
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'template' => $template,
|
||||
'context' => $context,
|
||||
];
|
||||
|
||||
return $this->render($templatePath, $templateOptions);
|
||||
@ -225,60 +245,57 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
||||
: [];
|
||||
|
||||
// if is test, render the data or generate the doc
|
||||
if ($isTest && isset($form) && true === $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
|
||||
$context
|
||||
->storeGenerated(
|
||||
$template,
|
||||
$storedObject,
|
||||
$entity,
|
||||
$contextGenerationData
|
||||
);
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
if ($isTest) {
|
||||
$creator = $contextGenerationData['creator'];
|
||||
$sendResultTo = ($form ?? null)?->get('send_result_to')?->getData() ?? null;
|
||||
$dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false;
|
||||
} else {
|
||||
$creator = $this->security->getUser();
|
||||
|
||||
if (!$creator instanceof User) {
|
||||
throw new AccessDeniedHttpException('only authenticated user can request a generation');
|
||||
}
|
||||
|
||||
$sendResultTo = null;
|
||||
$dumpOnly = false;
|
||||
}
|
||||
|
||||
$this->messageBus->dispatch(
|
||||
new RequestGenerationMessage(
|
||||
$this->security->getUser(),
|
||||
$creator,
|
||||
$template,
|
||||
$entityId,
|
||||
$storedObject,
|
||||
$contextGenerationDataSanitized,
|
||||
$isTest,
|
||||
$sendResultTo,
|
||||
$dumpOnly,
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -69,7 +69,7 @@ class DocGeneratorTemplate
|
||||
*
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private int $id;
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
|
@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class DocGeneratorTemplateRepository implements ObjectRepository
|
||||
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Repository;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @extends ObjectRepository<DocGeneratorTemplate>
|
||||
*/
|
||||
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function countByEntity(string $entity): int;
|
||||
}
|
@ -1,36 +1,62 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
{% block table_entities_thead_tr %}
|
||||
<th></th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
<th>{{ 'docgen.Context'|trans }}</th>
|
||||
<th>{{ 'docgen.test generate'|trans }}</th>
|
||||
<th>{{ 'Edit'|trans }}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_entities_tbody %}
|
||||
{% 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" />
|
||||
{% 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 %}
|
||||
|
||||
<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,18 +6,20 @@
|
||||
<div class="col-md-10 col-xxl">
|
||||
|
||||
<h1>{{ block('title') }}</h1>
|
||||
<div class="container">
|
||||
<div class="container overflow-hidden">
|
||||
{% for key, context in contexts %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="row g-3" style="margin-top: 1rem;">
|
||||
<div class="col-4 offset-1 text-center">
|
||||
<a
|
||||
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
||||
class="btn btn-outline-chill-green-dark">
|
||||
{{ context.name|trans }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{{ context.description|trans|nl2br }}
|
||||
<div class="col">
|
||||
<div>
|
||||
{{ context.description|trans|nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{{ creator.label }},
|
||||
{% if creator is not same as null %}{{ creator.label }},{% endif %}
|
||||
|
||||
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
|
||||
{{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }}
|
||||
|
||||
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
{{ 'docgen.data_dump_email.Dear'|trans }}
|
||||
|
||||
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
|
||||
|
||||
{{ link }}
|
||||
|
||||
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
|
@ -17,52 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Generator implements GeneratorInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[docgen generator] ';
|
||||
|
||||
public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager) {}
|
||||
public function __construct(
|
||||
private readonly ContextManagerInterface $contextManager,
|
||||
private readonly DriverInterface $driver,
|
||||
private readonly ManagerRegistry $objectManagerRegistry,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager
|
||||
) {
|
||||
}
|
||||
|
||||
public function generateDataDump(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject {
|
||||
return $this->generateFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataNormalized,
|
||||
$destinationStoredObject,
|
||||
$creator,
|
||||
$clearEntityManagerDuringProcess,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of File|null
|
||||
* @template B of bool
|
||||
*
|
||||
* @param B $isTest
|
||||
* @param (B is true ? T : null) $testFile
|
||||
*
|
||||
* @psalm-return (B is true ? string : null)
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||
*/
|
||||
public function generateDocFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
?StoredObject $destinationStoredObject = null,
|
||||
bool $isTest = false,
|
||||
?File $testFile = null,
|
||||
?User $creator = null
|
||||
): ?string {
|
||||
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject {
|
||||
return $this->generateFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataNormalized,
|
||||
$destinationStoredObject,
|
||||
$creator,
|
||||
$clearEntityManagerDuringProcess,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private function generateFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
bool $generateDumpOnly = false,
|
||||
): StoredObject {
|
||||
if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
||||
throw new ObjectReadyException();
|
||||
}
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||
|
||||
$entity = $this
|
||||
->entityManager
|
||||
->objectManagerRegistry
|
||||
->getManagerForClass($context->getEntityClass())
|
||||
->find($context->getEntityClass(), $entityId)
|
||||
;
|
||||
|
||||
@ -80,17 +116,47 @@ class Generator implements GeneratorInterface
|
||||
|
||||
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||
|
||||
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
||||
$this->entityManager->clear();
|
||||
gc_collect_cycles();
|
||||
if (null !== $destinationStoredObjectId) {
|
||||
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
||||
$destinationStoredObjectId = $destinationStoredObject->getId();
|
||||
|
||||
if ($clearEntityManagerDuringProcess) {
|
||||
// we clean the entity manager
|
||||
$this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();
|
||||
|
||||
// this will force php to clean the memory
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
if ($isTest && ($testFile instanceof File)) {
|
||||
$templateDecrypted = file_get_contents($testFile->getPathname());
|
||||
} else {
|
||||
// as we potentially deleted the storedObject from memory, we have to restore it
|
||||
$destinationStoredObject = $this->objectManagerRegistry
|
||||
->getManagerForClass(StoredObject::class)
|
||||
->find(StoredObject::class, $destinationStoredObjectId);
|
||||
|
||||
if ($generateDumpOnly) {
|
||||
$content = Yaml::dump($data, 6);
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType('application/yaml')
|
||||
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $content);
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
return $destinationStoredObject;
|
||||
}
|
||||
|
||||
try {
|
||||
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -103,19 +169,10 @@ class Generator implements GeneratorInterface
|
||||
$template->getFile()->getFilename()
|
||||
);
|
||||
} catch (TemplateException $e) {
|
||||
$destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors()));
|
||||
throw new GeneratorException($e->getErrors(), $e);
|
||||
}
|
||||
|
||||
if (true === $isTest) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||
'is_test' => true,
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
return $generatedResource;
|
||||
}
|
||||
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType($template->getFile()->getType())
|
||||
@ -123,15 +180,19 @@ class Generator implements GeneratorInterface
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
$this->entityManager->flush();
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
return $destinationStoredObject;
|
||||
}
|
||||
}
|
||||
|
@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
interface GeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @template T of File|null
|
||||
* @template B of bool
|
||||
* Generate a document and store the document on disk.
|
||||
*
|
||||
* @param B $isTest
|
||||
* @param (B is true ? T : null) $testFile
|
||||
* The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored
|
||||
* into the object. The number of generation trial will also be incremented.
|
||||
*
|
||||
* @psalm-return (B is true ? string : null)
|
||||
* This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process,
|
||||
* unless the paarameter `$clearEntityManagerDuringProcess` is set on false.
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||
* As the entity manager might be cleaned, the new instance of the stored object will be returned by this method.
|
||||
*
|
||||
* Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`).
|
||||
*
|
||||
* @phpstan-impure
|
||||
*
|
||||
* @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials
|
||||
*
|
||||
* @throws StoredObjectManagerException if unable to decrypt the template or store the document
|
||||
*/
|
||||
public function generateDocFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
?StoredObject $destinationStoredObject = null,
|
||||
bool $isTest = false,
|
||||
?File $testFile = null,
|
||||
?User $creator = null
|
||||
): ?string;
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject;
|
||||
|
||||
/**
|
||||
* Generate a data dump, and store it within the `$destinationStoredObject`.
|
||||
*/
|
||||
public function generateDataDump(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject;
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||
|
||||
/**
|
||||
* The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache
|
||||
* after a specific message is handled or fails.
|
||||
*/
|
||||
final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerMessageHandledEvent::class => [
|
||||
['afterHandling', 0],
|
||||
],
|
||||
WorkerMessageFailedEvent::class => [
|
||||
['afterFails', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function afterHandling(WorkerMessageHandledEvent $event): void
|
||||
{
|
||||
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$this->clearStoredObjectCache();
|
||||
}
|
||||
}
|
||||
|
||||
public function afterFails(WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$this->clearStoredObjectCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function clearStoredObjectCache(): void
|
||||
{
|
||||
$this->logger->debug('clear the cache after generation of a document');
|
||||
|
||||
$this->storedObjectManager->clearCache();
|
||||
}
|
||||
}
|
@ -11,10 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||
use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@ -24,11 +25,23 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @see OnGenerationFailsTest for test suite
|
||||
*/
|
||||
final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
{
|
||||
public const LOG_PREFIX = '[docgen failed] ';
|
||||
|
||||
public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository) {}
|
||||
public function __construct(
|
||||
private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
private MailerInterface $mailer,
|
||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||
private TranslatorInterface $translator,
|
||||
private UserRepositoryInterface $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
@ -43,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
if (!$message instanceof RequestGenerationMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var RequestGenerationMessage $message */
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
||||
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||
'entity_id' => $message->getEntityId(),
|
||||
@ -77,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
|
||||
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
$creatorId = $message->getCreatorId();
|
||||
|
||||
if (null === $creator = $this->userRepository->find($creatorId)) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $creator->getEmail() || '' === $creator->getEmail()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]);
|
||||
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'No email associated with this request generation');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -94,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
// if the exception is not a GeneratorException, we try the previous one...
|
||||
$throwable = $event->getThrowable();
|
||||
if (!$throwable instanceof GeneratorException) {
|
||||
$throwable = $throwable->getPrevious();
|
||||
$throwable = $throwable->getPrevious() ?? $throwable;
|
||||
}
|
||||
|
||||
if ($throwable instanceof GeneratorException) {
|
||||
@ -109,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $creator = $this->userRepository->find($message->getCreatorId())) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Creator not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->to($creator->getEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||
->context([
|
||||
|
@ -11,15 +11,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Handle the request of document generation.
|
||||
@ -30,7 +36,18 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
|
||||
private const LOG_PREFIX = '[docgen message handler] ';
|
||||
|
||||
public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository) {}
|
||||
public function __construct(
|
||||
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Generator $generator,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly StoredObjectRepository $storedObjectRepository,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RequestGenerationMessage $message)
|
||||
{
|
||||
@ -43,25 +60,59 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
}
|
||||
|
||||
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [
|
||||
'template_id' => $message->getTemplateId(),
|
||||
'destination_stored_object' => $message->getDestinationStoredObjectId(),
|
||||
'trial' => $destinationStoredObject->getGenerationTrialsCounter(),
|
||||
]);
|
||||
|
||||
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
|
||||
}
|
||||
|
||||
$creator = $this->userRepository->find($message->getCreatorId());
|
||||
|
||||
// we increase the number of generation trial in the object, and, in the same time, update the counter
|
||||
// on the database side. This ensure that, if the script fails for any reason (memory limit reached), the
|
||||
// counter is inscreased
|
||||
$destinationStoredObject->addGenerationTrial();
|
||||
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
||||
->setParameter('id', $destinationStoredObject->getId())
|
||||
->execute();
|
||||
|
||||
$this->generator->generateDocFromTemplate(
|
||||
$template,
|
||||
$message->getEntityId(),
|
||||
$message->getContextGenerationData(),
|
||||
$destinationStoredObject,
|
||||
false,
|
||||
null,
|
||||
$creator
|
||||
);
|
||||
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->logger->info(self::LOG_PREFIX.'Request generation finished', [
|
||||
'template_id' => $message->getTemplateId(),
|
||||
@ -69,4 +120,23 @@ 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,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class RequestGenerationMessage
|
||||
final readonly class RequestGenerationMessage
|
||||
{
|
||||
private readonly int $creatorId;
|
||||
private int $creatorId;
|
||||
|
||||
private readonly int $templateId;
|
||||
private int $templateId;
|
||||
|
||||
private readonly int $destinationStoredObjectId;
|
||||
private int $destinationStoredObjectId;
|
||||
|
||||
private readonly \DateTimeImmutable $createdAt;
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
private ?string $sendResultToEmail;
|
||||
|
||||
public function __construct(
|
||||
User $creator,
|
||||
DocGeneratorTemplate $template,
|
||||
private readonly int $entityId,
|
||||
private int $entityId,
|
||||
StoredObject $destinationStoredObject,
|
||||
private readonly array $contextGenerationData
|
||||
private array $contextGenerationData,
|
||||
private bool $isTest = false,
|
||||
?string $sendResultToEmail = null,
|
||||
private bool $dumpOnly = false,
|
||||
) {
|
||||
$this->creatorId = $creator->getId();
|
||||
$this->templateId = $template->getId();
|
||||
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
||||
$this->createdAt = new \DateTimeImmutable('now');
|
||||
$this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail();
|
||||
}
|
||||
|
||||
public function getCreatorId(): int
|
||||
@ -67,4 +73,19 @@ class RequestGenerationMessage
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function isTest(): bool
|
||||
{
|
||||
return $this->isTest;
|
||||
}
|
||||
|
||||
public function getSendResultToEmail(): ?string
|
||||
{
|
||||
return $this->sendResultToEmail;
|
||||
}
|
||||
|
||||
public function isDumpOnly(): bool
|
||||
{
|
||||
return $this->dumpOnly;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,9 @@ 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;
|
||||
@ -66,7 +68,11 @@ class GeneratorTest extends TestCase
|
||||
$entityManager->find('DummyClass', Argument::type('int'))
|
||||
->willReturn($entity);
|
||||
$entityManager->clear()->shouldBeCalled();
|
||||
$entityManager->flush()->shouldBeCalled();
|
||||
$entityManager->flush()->shouldNotBeCalled();
|
||||
|
||||
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||
@ -75,7 +81,7 @@ class GeneratorTest extends TestCase
|
||||
$generator = new Generator(
|
||||
$contextManagerInterface->reveal(),
|
||||
$driver->reveal(),
|
||||
$entityManager->reveal(),
|
||||
$managerRegistry->reveal(),
|
||||
new NullLogger(),
|
||||
$storedObjectManager->reveal()
|
||||
);
|
||||
@ -84,7 +90,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
|
||||
@ -95,7 +102,7 @@ class GeneratorTest extends TestCase
|
||||
$generator = new Generator(
|
||||
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
||||
$this->prophesize(DriverInterface::class)->reveal(),
|
||||
$this->prophesize(EntityManagerInterface::class)->reveal(),
|
||||
$this->prophesize(ManagerRegistry::class)->reveal(),
|
||||
new NullLogger(),
|
||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||
);
|
||||
@ -108,7 +115,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
|
||||
@ -136,10 +144,14 @@ 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(),
|
||||
$entityManager->reveal(),
|
||||
$managerRegistry->reveal(),
|
||||
new NullLogger(),
|
||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||
);
|
||||
@ -148,7 +160,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,107 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
docgen:
|
||||
data_dump_email:
|
||||
link_valid_until: >-
|
||||
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
|
@ -14,13 +14,31 @@ 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:
|
||||
@ -28,5 +46,4 @@ 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
|
||||
|
@ -24,6 +24,11 @@ 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")
|
||||
@ -116,6 +121,16 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -143,6 +158,11 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
*/
|
||||
public function getCreationDate(): \DateTime
|
||||
{
|
||||
if (null === $this->createdAt) {
|
||||
// this scenario will quite never happens
|
||||
return new \DateTime('now');
|
||||
}
|
||||
|
||||
return \DateTime::createFromImmutable($this->createdAt);
|
||||
}
|
||||
|
||||
@ -302,4 +322,37 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
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,11 +14,10 @@ namespace Chill\DocStoreBundle\Repository;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final class StoredObjectRepository implements ObjectRepository
|
||||
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
<?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
|
||||
{
|
||||
}
|
@ -105,6 +105,12 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
)
|
||||
: $clearContent;
|
||||
|
||||
$headers = [];
|
||||
|
||||
if (null !== $document->getDeleteAt()) {
|
||||
$headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp();
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this
|
||||
->client
|
||||
@ -119,6 +125,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
->url,
|
||||
[
|
||||
'body' => $encryptedContent,
|
||||
'headers' => $headers,
|
||||
]
|
||||
);
|
||||
} catch (TransportExceptionInterface $exception) {
|
||||
@ -130,6 +137,11 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->inMemory = [];
|
||||
}
|
||||
|
||||
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
||||
{
|
||||
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
|
||||
interface StoredObjectManagerInterface
|
||||
{
|
||||
@ -23,6 +24,8 @@ 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;
|
||||
|
||||
@ -31,6 +34,10 @@ 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;
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
@ -118,6 +118,41 @@ 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) {
|
@ -0,0 +1,36 @@
|
||||
<?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,6 +433,7 @@ 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|null
|
||||
public function getModerator(): ?User
|
||||
{
|
||||
return $this->moderator;
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get event.
|
||||
*/
|
||||
public function getEvent(): Event|null
|
||||
public function getEvent(): ?Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
@ -127,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get role.
|
||||
*/
|
||||
public function getRole(): Role|null
|
||||
public function getRole(): ?Role
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
@ -147,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get status.
|
||||
*/
|
||||
public function getStatus(): Status|null
|
||||
public function getStatus(): ?Status
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
@ -12,11 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Command;
|
||||
|
||||
use Chill\MainBundle\Entity\Language;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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;
|
||||
|
||||
/*
|
||||
@ -39,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
||||
/**
|
||||
* LoadCountriesCommand constructor.
|
||||
*/
|
||||
public function __construct(private readonly EntityManager $entityManager, private $availableLanguages)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@ -79,7 +80,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$em = $this->entityManager;
|
||||
$chillAvailableLanguages = $this->availableLanguages;
|
||||
$chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages');
|
||||
$languages = [];
|
||||
|
||||
foreach ($chillAvailableLanguages as $avLang) {
|
||||
@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
||||
$avLangNames = [];
|
||||
|
||||
foreach ($chillAvailableLanguages as $avLang) {
|
||||
$avLangNames[$avLang] = Languages::getName($code, $avLang);
|
||||
$avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang));
|
||||
}
|
||||
|
||||
$languageDB->setName($avLangNames);
|
||||
|
@ -47,4 +47,12 @@ 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');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
<?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, []);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
27
src/Bundle/ChillMainBundle/Controller/NewsItemController.php
Normal file
27
src/Bundle/ChillMainBundle/Controller/NewsItemController.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?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|null;
|
||||
public function run(array $lastExecutionData): ?array;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ 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;
|
||||
@ -53,6 +54,7 @@ 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;
|
||||
@ -62,6 +64,7 @@ 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;
|
||||
@ -544,6 +547,35 @@ 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,7 +12,6 @@ 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;
|
||||
|
||||
@ -40,15 +39,15 @@ class Age extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->value1 = $parser->SimpleArithmeticExpression();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->value2 = $parser->SimpleArithmeticExpression();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ 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;
|
||||
|
||||
@ -45,17 +44,17 @@ class Extract extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$this->field = $parser->getLexer()->token['value'];
|
||||
|
||||
$parser->match(Lexer::T_FROM);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_FROM);
|
||||
|
||||
// $this->value = $parser->ScalarExpression();
|
||||
$this->value = $parser->ArithmeticPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -33,11 +32,11 @@ class GetJsonFieldByKey extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->expr1 = $parser->StringPrimary();
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->expr2 = $parser->StringPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@ -41,15 +40,15 @@ class Greatest extends FunctionNode
|
||||
$this->exprs = [];
|
||||
|
||||
$lexer = $parser->getLexer();
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
|
||||
while (Lexer::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
}
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -33,9 +32,9 @@ class JsonAggregate extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->expr = $parser->StringPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@ -38,16 +37,16 @@ class JsonBuildObject extends FunctionNode
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$lexer = $parser->getLexer();
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
|
||||
while (Lexer::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
}
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -29,15 +28,15 @@ class JsonExtract extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->element = $parser->ArithmeticPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->keyToExtract = $parser->ArithmeticExpression();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -33,9 +32,9 @@ class JsonbArrayLength extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->expr1 = $parser->StringPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -33,11 +32,11 @@ class JsonbExistsInArray extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->expr1 = $parser->StringPrimary();
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->expr2 = $parser->InputParameter();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@ -41,15 +40,15 @@ class Least extends FunctionNode
|
||||
$this->exprs = [];
|
||||
|
||||
$lexer = $parser->getLexer();
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
|
||||
while (Lexer::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->exprs[] = $parser->ArithmeticPrimary();
|
||||
}
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
|
||||
/**
|
||||
@ -45,29 +44,29 @@ class OverlapsI extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->firstPeriodStart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->firstPeriodEnd = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->secondPeriodStart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->secondPeriodEnd = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
protected function makeCase($sqlWalker, $part, string $position): string
|
||||
|
@ -12,7 +12,6 @@ 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
|
||||
{
|
||||
@ -35,19 +34,19 @@ class Replace extends FunctionNode
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->string = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->from = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->to = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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.
|
||||
@ -31,15 +30,15 @@ class STContains extends FunctionNode
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->firstPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->secondPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -27,11 +26,11 @@ class STX extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->field = $parser->ArithmeticExpression();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -27,11 +26,11 @@ class STY extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->field = $parser->ArithmeticExpression();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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
|
||||
{
|
||||
@ -28,15 +27,15 @@ class Similarity extends FunctionNode
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->firstPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->secondPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\DQL;
|
||||
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
|
||||
@ -29,15 +28,15 @@ class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\Function
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->firstPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
|
||||
$this->secondPart = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@ -36,11 +35,11 @@ class ToChar extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->datetime = $parser->ArithmeticExpression();
|
||||
$parser->match(Lexer::T_COMMA);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
|
||||
$this->fmt = $parser->StringExpression();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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 :
|
||||
@ -31,11 +30,11 @@ class Unaccent extends FunctionNode
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->string = $parser->StringPrimary();
|
||||
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
112
src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php
Normal file
112
src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
128
src/Bundle/ChillMainBundle/Entity/NewsItem.php
Normal file
128
src/Bundle/ChillMainBundle/Entity/NewsItem.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -550,7 +550,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
$this->scopeHistories[] = $newScope;
|
||||
|
||||
$criteria = new Criteria();
|
||||
$criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]);
|
||||
$criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]);
|
||||
|
||||
/** @var \Iterator $scopes */
|
||||
$scopes = $this->scopeHistories->matching($criteria)->getIterator();
|
||||
@ -606,7 +606,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
$this->jobHistories[] = $newJob;
|
||||
|
||||
$criteria = new Criteria();
|
||||
$criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]);
|
||||
$criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]);
|
||||
|
||||
/** @var \Iterator $jobs */
|
||||
$jobs = $this->jobHistories->matching($criteria)->getIterator();
|
||||
|
56
src/Bundle/ChillMainBundle/Form/NewsItemType.php
Normal file
56
src/Bundle/ChillMainBundle/Form/NewsItemType.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
145
src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php
Normal file
145
src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?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,6 +12,7 @@ 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;
|
||||
@ -20,7 +21,7 @@ final class ScopeRepository implements ScopeRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(EntityManagerInterface $entityManager, private readonly TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(Scope::class);
|
||||
}
|
||||
@ -45,11 +46,11 @@ final class ScopeRepository implements ScopeRepositoryInterface
|
||||
|
||||
public function findAllActive(): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('s');
|
||||
$scopes = $this->repository->findBy(['active' => true]);
|
||||
|
||||
$qb->where('s.active = \'TRUE\'');
|
||||
usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName()));
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,7 +40,11 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
|
||||
public function findAllActive(): array
|
||||
{
|
||||
return $this->repository->findBy(['active' => true]);
|
||||
$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;
|
||||
}
|
||||
|
||||
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
|
||||
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_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_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
|
||||
|
@ -0,0 +1 @@
|
||||
import './index.scss';
|
@ -0,0 +1,7 @@
|
||||
div.flex-table {
|
||||
.news-content {
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -160,3 +160,11 @@ export interface LocationType {
|
||||
contactData: "optional" | "required";
|
||||
title: TranslatableString;
|
||||
}
|
||||
|
||||
export interface NewsItemType {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
startDate: DateTime;
|
||||
endDate: DateTime | null;
|
||||
}
|
||||
|
@ -97,6 +97,8 @@ 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",
|
||||
@ -112,7 +114,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'MyCustoms'
|
||||
activeTab: 'MyCustoms',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -126,8 +128,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
selectTab(tab) {
|
||||
this.$store.dispatch('getByTab', { tab: tab });
|
||||
if (tab !== 'MyCustoms') {
|
||||
this.$store.dispatch('getByTab', { tab: tab });
|
||||
}
|
||||
this.activeTab = tab;
|
||||
console.log(this.activeTab)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -0,0 +1,47 @@
|
||||
<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>
|
@ -0,0 +1,183 @@
|
||||
<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,76 +1,73 @@
|
||||
<template>
|
||||
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_dashboard') }}</span>
|
||||
<div v-else id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
|
||||
<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 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 Masonry from 'masonry-layout/masonry';
|
||||
import {makeFetch} from "ChillMainAssets/lib/api/apiMethods";
|
||||
import News from './DashboardWidgets/News.vue';
|
||||
|
||||
export default {
|
||||
name: "MyCustoms",
|
||||
components: {
|
||||
News
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
counterClass: {
|
||||
counter: true //hack to pass class 'counter' in i18n-t
|
||||
}
|
||||
},
|
||||
dashboardItems: [],
|
||||
masonry: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -78,11 +75,19 @@ export default {
|
||||
noResults() {
|
||||
return false
|
||||
},
|
||||
hasDashboardItems() {
|
||||
return this.dashboardItems.length > 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const elem = document.querySelector('#dashboards');
|
||||
const masonry = new Masonry(elem, {});
|
||||
}
|
||||
makeFetch('GET', '/api/1.0/main/dashboard-config-item.json')
|
||||
.then((response) => {
|
||||
this.dashboardItems = response;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -98,4 +103,10 @@ span.counter {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
div.news {
|
||||
max-height: 22rem;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
@ -63,7 +63,15 @@ const appMessages = {
|
||||
},
|
||||
emergency: "Urgent",
|
||||
confidential: "Confidentiel",
|
||||
automatic_notification: "Notification automatique"
|
||||
automatic_notification: "Notification automatique",
|
||||
widget: {
|
||||
news: {
|
||||
title: "Actualités",
|
||||
readMore: "Lire la suite",
|
||||
date: "Date",
|
||||
none: "Aucune actualité"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -96,13 +96,11 @@ 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);
|
||||
@ -221,7 +219,7 @@ const store = createStore({
|
||||
default:
|
||||
throw 'tab '+ tab;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue";
|
||||
|
||||
export interface AddressModalContentProps {
|
||||
address_id: number;
|
||||
address_ref_status: AddressRefStatus | null;
|
||||
address_ref_status: AddressRefStatus;
|
||||
}
|
||||
|
||||
const data = reactive<{
|
||||
loading: boolean,
|
||||
working_address: Address | null,
|
||||
working_ref_status: AddressRefStatus | null,
|
||||
}>({
|
||||
interface AddressModalData {
|
||||
loading: boolean,
|
||||
working_address: Address | null,
|
||||
working_ref_status: AddressRefStatus | null,
|
||||
}
|
||||
|
||||
const data: AddressModalData = reactive({
|
||||
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"
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %}
|
||||
|
||||
{% block vertical_menu_content %}
|
||||
{{ chill_menu('admin_news_item', {
|
||||
'layout': '@ChillMain/Admin/menu_admin_section.html.twig',
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block layout_wvm_content %}
|
||||
{% block admin_content %}<!-- block content empty -->
|
||||
<h1>{{ 'admin.dashboard.description' | trans }}</h1>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -1 +1 @@
|
||||
{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }}
|
||||
{{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user