Compare commits

...

29 Commits

Author SHA1 Message Date
a2cea3df02 Release 2.17.0 2024-03-19 21:00:38 +01:00
9ac43ecf5b Merge branch '238-custom-column-export-person' into 'master'
Liste des usagers: permettre d'ajouter des colonnes custom

Closes #238

See merge request Chill-Projet/chill-bundles!668
2024-03-19 19:59:38 +00:00
f78f5e8419 Fix cs with php-cs-fixer version 3.52 2024-03-19 20:49:39 +01:00
ccf3324bc2 Refactor ListPersonHelper and ListPerson to simplify process and allow to add customization of fields 2024-03-19 20:49:39 +01:00
dfe780f0f5 Merge branch '258-centers-parcours-export' into 'master'
In the accompanying period list, add person's centers and duration

Closes #258

See merge request Chill-Projet/chill-bundles!661
2024-03-14 21:35:00 +00:00
dd056efa0d In the accompanying period list, add person's centers and duration 2024-03-14 21:35:00 +00:00
18c0b6a47f Merge branch '259-Génération-de-document-avoir-un-comportement-coherent' into 'master'
Fix activity filter inconsistency in document generation

Closes #259

See merge request Chill-Projet/chill-bundles!667
2024-03-14 20:25:59 +00:00
df0afcd228 Fix activity filter inconsistency in document generation
This commit resolves issue 259 where the filtering of activities differed within the document generation and in the list of activities for an accompanying period. This amendment to the Chill Activity Bundle ensures consistent behavior. Additionally, new test methods and query adjustments were applied to the ActivityACLAwareRepository for better functionality.
2024-03-14 21:16:05 +01:00
d66933c8b5 Merge branch '264-repair-loading-languages' into 'master'
Fix the command which load language

Closes #264

See merge request Chill-Projet/chill-bundles!666
2024-03-14 15:12:30 +00:00
0ff51b0a5c Force new parameter to be readonly in LoadAndUpdateLanguagesCommand constructor 2024-03-14 15:05:30 +00:00
d7f4895248 Fix the command which load language
The command load the languages in the configured languages in chill's configuration.
2024-03-14 15:46:32 +01:00
7aee722957 Merge branch '237-filter-evaluations-between-dates' into 'master'
Resolve "Nouveau filtre: Filtrer les actions ayant reçu une nouvelle évaluation créée entre deux dates"

Closes #237

See merge request Chill-Projet/chill-bundles!663
2024-03-08 10:37:44 +00:00
5880858191 Resolve "Nouveau filtre: Filtrer les actions ayant reçu une nouvelle évaluation créée entre deux dates" 2024-03-08 10:37:43 +00:00
96105b101f Merge branch 'issue159_page_acceuil' into 'master'
Allow users to display news on homepage (+ configuring a dashboard homepage)

See merge request Chill-Projet/chill-bundles!604
2024-03-07 21:08:00 +00:00
d29415317b Allow users to display news on homepage (+ configuring a dashboard homepage) 2024-03-07 21:08:00 +00:00
2ad3bbe96f Merge branch '00585-fix-deprecations-doctrine-2024-03' into 'master'
Fix deprecations and code style issues (2024-03-07)

See merge request Chill-Projet/chill-bundles!665
2024-03-07 14:33:58 +00:00
1d636f5e9e Fix deprecations and code style issues 2024-03-07 15:26:58 +01:00
f0dbb17172 Update exports.rst: fix typo 2024-03-06 11:40:19 +00:00
f1dbc17dad Merge branch 'Doc-why-use-exists-in-exports' into 'master'
Update documentation to explain use of EXISTS in SQL queries

See merge request Chill-Projet/chill-bundles!664
2024-03-06 11:36:01 +00:00
09578a775c Update documentation to explain use of EXISTS in SQL queries
Added an explanatory section to the "exports.rst" doc to clarify why to use an EXISTS subquery instead of a JOIN clause in SQL queries involving many-to-* relationships. This explanation includes sample SQL queries and results to illustrate the potential issue of duplicates with JOIN and count, and how EXISTS can help avoid this issue. Also updated the ".editorconfig" file for .rst files.
2024-03-06 12:34:36 +01:00
c888b5b84f Update chill bundles version to 2.16.3 2024-02-26 14:53:20 +01:00
27d76d9579 Merge branch '232-filters-uj-and-serv-order-alphabetical' into 'master'
Resolve "Filtres sur les données: classer par ordre alphabétique les items à sélectionner"

Closes #232

See merge request Chill-Projet/chill-bundles!662
2024-02-26 13:51:25 +00:00
5b714f17be order scopes alphabetically 2024-02-26 14:40:41 +01:00
bbb167bb85 order user jobs alphabetically when returning all active user jobs 2024-02-26 13:36:44 +01:00
d713087dcb Changie and php style fixes 2024-02-26 13:30:26 +01:00
569aeeef87 Fix wrong translation of user job service -> métier 2024-02-26 12:23:11 +01:00
97f2c75de8 Change syntax of check on null for closing motive 2024-02-21 20:14:18 +01:00
4a2078dc65 upgrade to 2.16.2 2024-02-21 19:49:43 +01:00
00444e1e56 Add check on null value in template for closing motive 2024-02-20 10:10:44 +01:00
127 changed files with 2710 additions and 504 deletions

3
.changes/v2.16.2.md Normal file
View 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
View 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

8
.changes/v2.17.0.md Normal file
View File

@@ -0,0 +1,8 @@
## 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
### 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

View File

@@ -23,3 +23,7 @@ max_line_length = 0
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
[.rst]
ident_size = 3
ident_style = space

View File

@@ -35,7 +35,7 @@ variables:
# force a timezone # force a timezone
TZ: Europe/Brussels TZ: Europe/Brussels
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations # avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=0 SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1
stages: stages:
- Composer install - Composer install

View File

@@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## 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 ## v2.16.1 - 2024-02-09
### Fixed ### Fixed
* Force bootstrap version to avoid error in builds with newer version * Force bootstrap version to avoid error in builds with newer version

View File

@@ -242,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati
Continue to explain the export framework Continue to explain the export framework
.. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main .. _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

View File

@@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface
{ {
$builder $builder
->add('jobs', EntityType::class, [ ->add('jobs', EntityType::class, [
'choices' => $this->userJobRepository->findAllOrderedByName(), 'choices' => $this->userJobRepository->findAllActive(),
'class' => UserJob::class, 'class' => UserJob::class,
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize( 'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
$s->getLabel() $s->getLabel()

View File

@@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface
private const PREFIX = 'acp_act_filter_creator_scope'; private const PREFIX = 'acp_act_filter_creator_scope';
public function __construct( public function __construct(
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
private readonly ScopeRepositoryInterface $scopeRepository,
) { ) {
} }
@@ -76,6 +78,7 @@ class CreatorScopeFilter implements FilterInterface
$builder $builder
->add('scopes', EntityType::class, [ ->add('scopes', EntityType::class, [
'class' => Scope::class, 'class' => Scope::class,
'choices' => $this->scopeRepository->findAllActive(),
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
$s->getName() $s->getName()
), ),

View File

@@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -27,7 +28,8 @@ class UsersJobFilter implements FilterInterface
private const PREFIX = 'act_filter_user_job'; private const PREFIX = 'act_filter_user_job';
public function __construct( public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository
) { ) {
} }
@@ -69,6 +71,7 @@ class UsersJobFilter implements FilterInterface
$builder $builder
->add('jobs', EntityType::class, [ ->add('jobs', EntityType::class, [
'class' => UserJob::class, 'class' => UserJob::class,
'choices' => $this->userJobRepository->findAllActive(),
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
'multiple' => true, 'multiple' => true,
'expanded' => true, 'expanded' => true,

View File

@@ -95,7 +95,7 @@ class ActivityType extends AbstractType
]); ]);
} }
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */ /** @var AccompanyingPeriod|null $accompanyingPeriod */
$accompanyingPeriod = null; $accompanyingPeriod = null;
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) { if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {

View File

@@ -243,7 +243,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
thirdparties.thirdpartyids, thirdparties.thirdpartyids,
persons.personids, persons.personids,
actions.socialactionids, actions.socialactionids,
issues.socialissueids issues.socialissueids,
a.user_id
FROM activity a FROM activity a
LEFT JOIN chill_main_location location ON a.location_id = location.id LEFT JOIN chill_main_location location ON a.location_id = location.id
@@ -283,6 +284,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
->addFieldResult('activityPresence', 'presence_id', 'id') ->addFieldResult('activityPresence', 'presence_id', 'id')
->addFieldResult('activityPresence', 'presence_name', 'name') ->addFieldResult('activityPresence', 'presence_name', 'name')
->addScalarResult('user_id', 'userId', Types::INTEGER)
// results which cannot be mapped into entity // results which cannot be mapped into entity
->addScalarResult('comment_comment', 'comment', Types::TEXT) ->addScalarResult('comment_comment', 'comment', Types::TEXT)

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Service\DocGenerator; namespace Chill\ActivityBundle\Service\DocGenerator;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Entity\ActivityPresence;
use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Entity\ActivityType;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
@@ -112,7 +113,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
} }
/** /**
* @return list * @return list<Activity>
*/ */
private function filterActivitiesByUser(array $activities, User $user): array private function filterActivitiesByUser(array $activities, User $user): array
{ {
@@ -120,6 +121,12 @@ class ListActivitiesByAccompanyingPeriodContext implements
array_filter( array_filter(
$activities, $activities,
function ($activity) use ($user) { 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'] ?? []); $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []);
return \in_array($user->getUsername(), $activityUsernames, true); return \in_array($user->getUsername(), $activityUsernames, true);
@@ -129,7 +136,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
} }
/** /**
* @return list * @return list<AccompanyingPeriod\AccompanyingPeriodWork>
*/ */
private function filterWorksByUser(array $works, User $user): array private function filterWorksByUser(array $works, User $user): array
{ {
@@ -216,6 +223,15 @@ class ListActivitiesByAccompanyingPeriodContext implements
foreach ($activities as $row) { foreach ($activities as $row) {
$activity = $row[0]; $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', [ $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class, AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class,
]); ]);

View File

@@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
self::assertIsArray($actual); 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 * @dataProvider provideDataFindByAccompanyingPeriod
*/ */
@@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
->getQuery() ->getQuery()
->getResult() ->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 if (null === $user = $this->entityManager

View File

@@ -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;
}
}

View File

@@ -396,7 +396,7 @@ export:
by_creator_job: by_creator_job:
job_form_label: Métiers job_form_label: Métiers
Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange 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: by_persons:
Filter activity by persons: Filtrer les échanges par usager participant Filter activity by persons: Filtrer les échanges par usager participant
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%' 'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'

View File

@@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface
private const PREFIX = 'aside_act_filter_user_job'; private const PREFIX = 'aside_act_filter_user_job';
public function __construct( public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository
) { ) {
} }
@@ -69,6 +71,7 @@ class ByUserJobFilter implements FilterInterface
$builder $builder
->add('jobs', EntityType::class, [ ->add('jobs', EntityType::class, [
'class' => UserJob::class, 'class' => UserJob::class,
'choices' => $this->userJobRepository->findAllActive(),
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
'multiple' => true, 'multiple' => true,
'expanded' => true, 'expanded' => true,

View File

@@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -26,7 +27,8 @@ final readonly class JobFilter implements FilterInterface
private const PREFIX = 'cal_filter_job'; private const PREFIX = 'cal_filter_job';
public function __construct( public function __construct(
private TranslatableStringHelper $translatableStringHelper private TranslatableStringHelper $translatableStringHelper,
private UserJobRepositoryInterface $userJobRepository
) { ) {
} }
@@ -74,6 +76,7 @@ final readonly class JobFilter implements FilterInterface
$builder $builder
->add('job', EntityType::class, [ ->add('job', EntityType::class, [
'class' => UserJob::class, 'class' => UserJob::class,
'choices' => $this->userJobRepository->findAllActive(),
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize(
$j->getLabel() $j->getLabel()
), ),

View File

@@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -28,7 +29,8 @@ class ScopeFilter implements FilterInterface
public function __construct( public function __construct(
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
private readonly ScopeRepositoryInterface $scopeRepository
) { ) {
} }
@@ -76,6 +78,7 @@ class ScopeFilter implements FilterInterface
$builder $builder
->add('scope', EntityType::class, [ ->add('scope', EntityType::class, [
'class' => Scope::class, 'class' => Scope::class,
'choices' => $this->scopeRepository->findAllActive(),
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
$s->getName() $s->getName()
), ),

View File

@@ -33,7 +33,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface
/** /**
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft * @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); $id = $this->mapCalendarToUser->getUserId($user);

View File

@@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface
/** /**
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft * @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;
} }

View File

@@ -433,6 +433,7 @@ final class EventController extends AbstractController
$builder->add('event_id', HiddenType::class, [ $builder->add('event_id', HiddenType::class, [
'data' => $event->getId(), 'data' => $event->getId(),
]); ]);
dump($event->getId());
return $builder->getForm(); return $builder->getForm();
} }

View File

@@ -197,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
return $this->id; return $this->id;
} }
public function getModerator(): User|null public function getModerator(): ?User
{ {
return $this->moderator; return $this->moderator;
} }

View File

@@ -91,7 +91,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/** /**
* Get event. * Get event.
*/ */
public function getEvent(): Event|null public function getEvent(): ?Event
{ {
return $this->event; return $this->event;
} }
@@ -127,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/** /**
* Get role. * Get role.
*/ */
public function getRole(): Role|null public function getRole(): ?Role
{ {
return $this->role; return $this->role;
} }
@@ -147,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/** /**
* Get status. * Get status.
*/ */
public function getStatus(): Status|null public function getStatus(): ?Status
{ {
return $this->status; return $this->status;
} }

View File

@@ -12,11 +12,12 @@ declare(strict_types=1);
namespace Chill\MainBundle\Command; namespace Chill\MainBundle\Command;
use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Language;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Intl\Languages; use Symfony\Component\Intl\Languages;
/* /*
@@ -39,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command
/** /**
* LoadCountriesCommand constructor. * LoadCountriesCommand constructor.
*/ */
public function __construct(private readonly EntityManager $entityManager, private $availableLanguages) public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag)
{ {
parent::__construct(); parent::__construct();
} }
@@ -79,7 +80,7 @@ class LoadAndUpdateLanguagesCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$em = $this->entityManager; $em = $this->entityManager;
$chillAvailableLanguages = $this->availableLanguages; $chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages');
$languages = []; $languages = [];
foreach ($chillAvailableLanguages as $avLang) { foreach ($chillAvailableLanguages as $avLang) {
@@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command
$avLangNames = []; $avLangNames = [];
foreach ($chillAvailableLanguages as $avLang) { foreach ($chillAvailableLanguages as $avLang) {
$avLangNames[$avLang] = Languages::getName($code, $avLang); $avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang));
} }
$languageDB->setName($avLangNames); $languageDB->setName($avLangNames);

View File

@@ -47,4 +47,12 @@ class AdminController extends AbstractController
{ {
return $this->render('@ChillMain/Admin/indexUser.html.twig'); 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');
}
} }

View File

@@ -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, []);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -28,5 +28,5 @@ interface CronJobInterface
* *
* @return array|null optionally return an array with the same data than the previous execution * @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;
} }

View File

@@ -19,6 +19,7 @@ use Chill\MainBundle\Controller\CountryController;
use Chill\MainBundle\Controller\LanguageController; use Chill\MainBundle\Controller\LanguageController;
use Chill\MainBundle\Controller\LocationController; use Chill\MainBundle\Controller\LocationController;
use Chill\MainBundle\Controller\LocationTypeController; use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\NewsItemController;
use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobApiController;
@@ -53,6 +54,7 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType; use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
@@ -62,6 +64,7 @@ use Chill\MainBundle\Form\CountryType;
use Chill\MainBundle\Form\LanguageType; use Chill\MainBundle\Form\LanguageType;
use Chill\MainBundle\Form\LocationFormType; use Chill\MainBundle\Form\LocationFormType;
use Chill\MainBundle\Form\LocationTypeType; use Chill\MainBundle\Form\LocationTypeType;
use Chill\MainBundle\Form\NewsItemType;
use Chill\MainBundle\Form\RegroupmentType; use Chill\MainBundle\Form\RegroupmentType;
use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserJobType;
use Chill\MainBundle\Form\UserType; 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' => [ 'apis' => [
[ [

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -40,15 +39,15 @@ class Age extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->value1 = $parser->SimpleArithmeticExpression(); $this->value1 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->value2 = $parser->SimpleArithmeticExpression(); $this->value2 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -14,7 +14,6 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\DateDiffFunction; use Doctrine\ORM\Query\AST\Functions\DateDiffFunction;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\PathExpression; use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -45,17 +44,17 @@ class Extract extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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);
$parser->match(Lexer::T_IDENTIFIER); $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
$this->field = $parser->getLexer()->token['value']; $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->ScalarExpression();
$this->value = $parser->ArithmeticPrimary(); $this->value = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -33,11 +32,11 @@ class GetJsonFieldByKey extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->expr1 = $parser->StringPrimary(); $this->expr1 = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->expr2 = $parser->StringPrimary(); $this->expr2 = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -41,15 +40,15 @@ class Greatest extends FunctionNode
$this->exprs = []; $this->exprs = [];
$lexer = $parser->getLexer(); $lexer = $parser->getLexer();
$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->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
while (Lexer::T_COMMA === $lexer->lookahead['type']) { while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
} }
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -33,9 +32,9 @@ class JsonAggregate extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->expr = $parser->StringPrimary(); $this->expr = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -38,16 +37,16 @@ class JsonBuildObject extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$lexer = $parser->getLexer(); $lexer = $parser->getLexer();
$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->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
while (Lexer::T_COMMA === $lexer->lookahead['type']) { while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
} }
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -29,15 +28,15 @@ class JsonExtract extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->element = $parser->ArithmeticPrimary(); $this->element = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->keyToExtract = $parser->ArithmeticExpression(); $this->keyToExtract = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -33,9 +32,9 @@ class JsonbArrayLength extends FunctionNode
public function parse(Parser $parser): void 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->expr1 = $parser->StringPrimary(); $this->expr1 = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -33,11 +32,11 @@ class JsonbExistsInArray extends FunctionNode
public function parse(Parser $parser): void 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->expr1 = $parser->StringPrimary(); $this->expr1 = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->expr2 = $parser->InputParameter(); $this->expr2 = $parser->InputParameter();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -41,15 +40,15 @@ class Least extends FunctionNode
$this->exprs = []; $this->exprs = [];
$lexer = $parser->getLexer(); $lexer = $parser->getLexer();
$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->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
while (Lexer::T_COMMA === $lexer->lookahead['type']) { while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) {
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->exprs[] = $parser->ArithmeticPrimary(); $this->exprs[] = $parser->ArithmeticPrimary();
} }
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\PathExpression; use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
/** /**
@@ -45,29 +44,29 @@ class OverlapsI extends FunctionNode
public function parse(Parser $parser): void 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(); $this->firstPeriodStart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->firstPeriodEnd = $parser->StringPrimary(); $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(); $this->secondPeriodStart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->secondPeriodEnd = $parser->StringPrimary(); $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 protected function makeCase($sqlWalker, $part, string $position): string

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
class Replace extends FunctionNode class Replace extends FunctionNode
{ {
@@ -35,19 +34,19 @@ class Replace extends FunctionNode
public function parse(\Doctrine\ORM\Query\Parser $parser): void public function parse(\Doctrine\ORM\Query\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->string = $parser->StringPrimary(); $this->string = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->from = $parser->StringPrimary(); $this->from = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->to = $parser->StringPrimary(); $this->to = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/** /**
* Geometry function 'ST_CONTAINS', added by postgis. * Geometry function 'ST_CONTAINS', added by postgis.
@@ -31,15 +30,15 @@ class STContains extends FunctionNode
public function parse(\Doctrine\ORM\Query\Parser $parser) public function parse(\Doctrine\ORM\Query\Parser $parser)
{ {
$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->firstPart = $parser->StringPrimary(); $this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->secondPart = $parser->StringPrimary(); $this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -27,11 +26,11 @@ class STX extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->field = $parser->ArithmeticExpression(); $this->field = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -27,11 +26,11 @@ class STY extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->field = $parser->ArithmeticExpression(); $this->field = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
class Similarity extends FunctionNode class Similarity extends FunctionNode
{ {
@@ -28,15 +27,15 @@ class Similarity extends FunctionNode
public function parse(\Doctrine\ORM\Query\Parser $parser) public function parse(\Doctrine\ORM\Query\Parser $parser)
{ {
$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->firstPart = $parser->StringPrimary(); $this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->secondPart = $parser->StringPrimary(); $this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -29,15 +28,15 @@ class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\Function
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->firstPart = $parser->StringPrimary(); $this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->secondPart = $parser->StringPrimary(); $this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
@@ -36,11 +35,11 @@ class ToChar extends FunctionNode
public function parse(Parser $parser) public function parse(Parser $parser)
{ {
$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->datetime = $parser->ArithmeticExpression(); $this->datetime = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA); $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA);
$this->fmt = $parser->StringExpression(); $this->fmt = $parser->StringExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Doctrine\DQL; namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/** /**
* Unaccent string using postgresql extension unaccent : * Unaccent string using postgresql extension unaccent :
@@ -31,11 +30,11 @@ class Unaccent extends FunctionNode
public function parse(\Doctrine\ORM\Query\Parser $parser) public function parse(\Doctrine\ORM\Query\Parser $parser)
{ {
$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->string = $parser->StringPrimary(); $this->string = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS);
} }
} }

View 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;
}
}

View 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;
}
}

View File

@@ -549,7 +549,7 @@ class User implements UserInterface, \Stringable
$this->scopeHistories[] = $newScope; $this->scopeHistories[] = $newScope;
$criteria = new Criteria(); $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 */ /** @var \Iterator $scopes */
$scopes = $this->scopeHistories->matching($criteria)->getIterator(); $scopes = $this->scopeHistories->matching($criteria)->getIterator();
@@ -605,7 +605,7 @@ class User implements UserInterface, \Stringable
$this->jobHistories[] = $newJob; $this->jobHistories[] = $newJob;
$criteria = new Criteria(); $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 */ /** @var \Iterator $jobs */
$jobs = $this->jobHistories->matching($criteria)->getIterator(); $jobs = $this->jobHistories->matching($criteria)->getIterator();

View 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);
}
}

View 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;
}
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository; namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -20,7 +21,7 @@ final class ScopeRepository implements ScopeRepositoryInterface
{ {
private readonly EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager, private readonly TranslatableStringHelperInterface $translatableStringHelper)
{ {
$this->repository = $entityManager->getRepository(Scope::class); $this->repository = $entityManager->getRepository(Scope::class);
} }
@@ -45,11 +46,11 @@ final class ScopeRepository implements ScopeRepositoryInterface
public function findAllActive(): array 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;
} }
/** /**

View File

@@ -40,7 +40,11 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
public function findAllActive(): array 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 public function findAllOrderedByName(): array

View File

@@ -0,0 +1 @@
import './index.scss';

View File

@@ -0,0 +1,7 @@
div.flex-table {
.news-content {
p {
margin-top: 1rem;
}
}
}

View File

@@ -160,3 +160,11 @@ export interface LocationType {
contactData: "optional" | "required"; contactData: "optional" | "required";
title: TranslatableString; title: TranslatableString;
} }
export interface NewsItemType {
id: number;
title: string;
content: string;
startDate: DateTime;
endDate: DateTime | null;
}

View File

@@ -97,6 +97,8 @@ import MyNotifications from './MyNotifications';
import MyWorkflows from './MyWorkflows.vue'; import MyWorkflows from './MyWorkflows.vue';
import TabCounter from './TabCounter'; import TabCounter from './TabCounter';
import { mapState } from "vuex"; import { mapState } from "vuex";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export default { export default {
name: "App", name: "App",
@@ -112,7 +114,7 @@ export default {
}, },
data() { data() {
return { return {
activeTab: 'MyCustoms' activeTab: 'MyCustoms',
} }
}, },
computed: { computed: {
@@ -126,8 +128,11 @@ export default {
}, },
methods: { methods: {
selectTab(tab) { selectTab(tab) {
this.$store.dispatch('getByTab', { tab: tab }); if (tab !== 'MyCustoms') {
this.$store.dispatch('getByTab', { tab: tab });
}
this.activeTab = tab; this.activeTab = tab;
console.log(this.activeTab)
} }
}, },
mounted() { mounted() {

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,76 +1,73 @@
<template> <template>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_dashboard') }}</span> <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"> <template v-if="this.hasDashboardItems">
<div class="custom1"> <template v-for="dashboardItem in this.dashboardItems">
<ul class="list-unstyled"> <div class="mbloc col-xs-12 col-sm-8 news" v-if="dashboardItem.type === 'news'">
<li v-if="counter.notifications > 0"> <News />
<i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications"> </div>
<template v-slot:n><span>{{ counter.notifications }}</span></template> </template>
</i18n-t> </template>
</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>
-->
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; 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 { export default {
name: "MyCustoms", name: "MyCustoms",
components: {
News
},
data() { data() {
return { return {
counterClass: { counterClass: {
counter: true //hack to pass class 'counter' in i18n-t counter: true //hack to pass class 'counter' in i18n-t
} },
dashboardItems: [],
masonry: null,
} }
}, },
computed: { computed: {
@@ -78,11 +75,19 @@ export default {
noResults() { noResults() {
return false return false
}, },
hasDashboardItems() {
return this.dashboardItems.length > 0;
}
}, },
mounted() { mounted() {
const elem = document.querySelector('#dashboards'); makeFetch('GET', '/api/1.0/main/dashboard-config-item.json')
const masonry = new Masonry(elem, {}); .then((response) => {
} this.dashboardItems = response;
})
.catch((error) => {
throw error
});
},
} }
</script> </script>
@@ -98,4 +103,10 @@ span.counter {
background-color: unset; background-color: unset;
} }
} }
div.news {
max-height: 22rem;
overflow: hidden;
overflow-y: scroll;
}
</style> </style>

View File

@@ -63,7 +63,15 @@ const appMessages = {
}, },
emergency: "Urgent", emergency: "Urgent",
confidential: "Confidentiel", confidential: "Confidentiel",
automatic_notification: "Notification automatique" automatic_notification: "Notification automatique",
widget: {
news: {
title: "Actualités",
readMore: "Lire la suite",
date: "Date",
none: "Aucune actualité"
}
}
} }
}; };

View File

@@ -96,13 +96,11 @@ const store = createStore({
}, },
catchError(state, error) { catchError(state, error) {
state.errorMsg.push(error); state.errorMsg.push(error);
} },
}, },
actions: { actions: {
getByTab({ commit, getters }, { tab, param }) { getByTab({ commit, getters }, { tab, param }) {
switch (tab) { switch (tab) {
case 'MyCustoms':
break;
// case 'MyWorks': // case 'MyWorks':
// if (!getters.isWorksLoaded) { // if (!getters.isWorksLoaded) {
// commit('setLoading', true); // commit('setLoading', true);
@@ -221,7 +219,7 @@ const store = createStore({
default: default:
throw 'tab '+ tab; throw 'tab '+ tab;
} }
} },
}, },
}); });

View File

@@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue";
export interface AddressModalContentProps { export interface AddressModalContentProps {
address_id: number; address_id: number;
address_ref_status: AddressRefStatus | null; address_ref_status: AddressRefStatus;
} }
const data = reactive<{ interface AddressModalData {
loading: boolean, loading: boolean,
working_address: Address | null, working_address: Address | null,
working_ref_status: AddressRefStatus | null, working_ref_status: AddressRefStatus | null,
}>({ }
const data: AddressModalData = reactive({
loading: false, loading: false,
working_address: null, working_address: null,
working_ref_status: null, working_ref_status: null,
}); } as AddressModalData);
const props = defineProps<AddressModalContentProps>(); const props = defineProps<AddressModalContentProps>();

View File

@@ -51,7 +51,7 @@ const messages = {
years_old: "1 an | {n} an | {n} ans", years_old: "1 an | {n} an | {n} ans",
residential_address: "Adresse de résidence", residential_address: "Adresse de résidence",
located_at: "réside chez" located_at: "réside chez"
} },
} }
}; };

View File

@@ -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 %}

View File

@@ -1 +1 @@
{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }} {{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }}

View File

@@ -0,0 +1,30 @@
<div class="item-bloc">
<div class="item-row">
<h3>
{{ entity.title }}
</h3>
</div>
<div class="item-row">
<p>
{% if entity.startDate %}
<span>{{ entity.startDate|format_date('long') }}</span>
{% endif %}
{% if entity.endDate %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</p>
</div>
<div class="item-row separator">
<div>
{{ entity.content|u.truncate(350, '… [' ~ ('news.read_more'|trans) ~ '](' ~ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) ~ ')', false)|chill_markdown_to_html }}
</div>
</div>
<div class="item-row">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) }}" class="btn btn-show btn-mini"></a>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="flex-table">
<div class="item-bloc">
<p class="date-label">
<span>{{ entity.startDate|format_date('long') }}</span>
{% if entity.endDate is not null %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</p>
</div>
<div class="item-bloc">
<div class="news-content">
{{ entity.content|chill_markdown_to_html }}
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_delete_content.html.twig' %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,43 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>{{ 'Title'|trans }}</th>
<th>{{ 'news.startDate'|trans }}</th>
<th>{{ 'news.endDate'|trans }}</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.title }}</td>
<td>{{ entity.startDate|format_date('long') }}</td>
{% if entity.endDate is not null %}
<td>{{ entity.endDate|format_date('long') }}</td>
{% else %}
<td>{{ 'news.noDate'|trans }}</td>
{% endif %}
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_news_item_view', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_delete', { 'id': entity.id }) }}" class="btn btn-delete" title="{{ 'delete'|trans }}"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,70 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}
{{ 'news.title'|trans }}
{% endblock title %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_news') }}
{% endblock %}
{% block content %}
<div class="col-md-10 asideactivity-list">
<h2>{{ 'news.title'|trans }}</h2>
{{ filter_order|chill_render_filter_order_helper }}
{% if entities|length == 0 %}
<p class="chill-no-data-statement">
{{ "news.no_data"|trans }}
</p>
{% else %}
<div class="flex-table">
{% for entity in entities %}
<div class="item-bloc">
<div class="item-row">
<div class="wrap-list">
<div class="wl-row">
<div class="wl-col">
<h2>{{ entity.title }}</h2>
</div>
<div class="wl-col">
<p>
{% if entity.startDate %}
<span>{{ entity.startDate|format_date('long') }}</span>
{% endif %}
{% if entity.endDate %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
<div class="item-row separator">
<div>
{{ entity.content|u.truncate(350, '…', false)|chill_markdown_to_html }}
</div>
</div>
{% if entity.content|length > 350 %}
<div class="item-row">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) }}" class="btn btn-show btn-sm read-more">{{ 'news.read_more'|trans }}</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{{ chill_pagination(paginator) }}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'news.show_details'|trans %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_news') }}
{% endblock %}
{% block content %}
<div class="col-md-10 col-xxl">
<div class="news-item-show">
<h1>{{ entity.title }}</h1>
{% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_news_items_history') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
</li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_view_title.html.twig') %}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_news') }}
{% endblock %}
{% block admin_content %}
<div class="col-md-10 col-xxl">
<div class="news-item-show">
<h1>{{ entity.title }}</h1>
{% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_crud_news_item_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_delete', { 'id': entity.id }) }}" class="btn btn-delete" title="{{ 'delete'|trans }}"></a>
</li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,47 @@
<?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\Routing\MenuBuilder;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class AdminNewsMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker)
{
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return;
}
$menu->addChild('admin.dashboard.title', [
'route' => 'chill_main_dashboard_admin',
])
->setAttribute('class', 'list-group-item-header')
->setExtras([
'order' => 9000,
]);
$menu->addChild('admin.dashboard.news', [
'route' => 'chill_crud_news_item_index',
])->setExtras(['order' => 9000]);
}
public static function getMenuIds(): array
{
return ['admin_section', 'admin_news_item'];
}
}

View File

@@ -60,6 +60,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'order' => 20, 'order' => 20,
]); ]);
} }
$menu->addChild($this->translator->trans('news.menu'), [
'route' => 'chill_main_news_items_history',
])
->setExtras([
'icons' => ['newspaper-o'],
'order' => 5,
]);
} }
public static function getMenuIds(): array public static function getMenuIds(): array

View File

@@ -41,7 +41,7 @@ final readonly class CollateAddressWithReferenceOrPostalCodeCronJob implements C
return 'collate-address'; return 'collate-address';
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
$maxId = ($this->collateAddressWithReferenceOrPostalCode)($lastExecutionData[self::LAST_MAX_ID] ?? 0); $maxId = ($this->collateAddressWithReferenceOrPostalCode)($lastExecutionData[self::LAST_MAX_ID] ?? 0);

View File

@@ -46,7 +46,7 @@ final readonly class RefreshAddressToGeographicalUnitMaterializedViewCronJob imp
return 'refresh-materialized-view-address-to-geog-units'; return 'refresh-materialized-view-address-to-geog-units';
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
$this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); $this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit');

View File

@@ -0,0 +1,35 @@
<?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\Templating\Entity;
use Chill\MainBundle\Entity\NewsItem;
/**
* @implements ChillEntityRenderInterface<NewsItem>
*/
final readonly class NewsItemRender implements ChillEntityRenderInterface
{
public function renderBox($entity, array $options): string
{
return '';
}
public function renderString($entity, array $options): string
{
return $entity->getTitle();
}
public function supports($newsItem, array $options): bool
{
return $newsItem instanceof NewsItem;
}
}

View File

@@ -0,0 +1,39 @@
<?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 Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
*
* @coversNothing
*/
class NewsItemApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function testListCurrentNewsItems()
{
$client = $this->getClientAuthenticated();
$client->request('GET', '/api/1.0/main/news/current.json');
$this->assertResponseIsSuccessful('Testing whether the GET request to the news item Api endpoint was successful');
$responseContent = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!empty($responseContent['data'][0])) {
$this->assertArrayHasKey('title', $responseContent['data'][0]);
}
}
}

View File

@@ -0,0 +1,96 @@
<?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 Controller;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Tests the admin pages for news items.
*
* @internal
*
* @coversNothing
*/
class NewsItemControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @var list<array{0: class-string, 1: int}>
*/
private static array $entitiesToDelete = [];
private readonly EntityManagerInterface $em;
protected function tearDown(): void
{
self::ensureKernelShutdown();
}
public static function tearDownAfterClass(): void
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
foreach (self::$entitiesToDelete as [$class, $id]) {
$entity = $em->find($class, $id);
if (null !== $entity) {
$em->remove($entity);
}
}
$em->flush();
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$newsItem = new NewsItem();
$newsItem->setTitle('Lorem Ipsum');
$newsItem->setContent('some text');
$newsItem->setStartDate(new \DateTimeImmutable('now'));
$em->persist($newsItem);
$em->flush();
self::$entitiesToDelete[] = [NewsItem::class, $newsItem];
self::ensureKernelShutdown();
yield [$newsItem];
}
public function testList()
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('News item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(NewsItem $newsItem)
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view");
self::assertResponseIsSuccessful('Single news item admin page loads successfully');
}
}

View File

@@ -0,0 +1,97 @@
<?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 Controller;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
*
* @coversNothing
*/
class NewsItemsHistoryControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @var list<array{0: class-string, 1: NewsItem}>
*/
private static array $toDelete = [];
public static function tearDownAfterClass(): void
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
foreach (static::$toDelete as [$class, $entity]) {
$query = $em->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class))
->setParameter('id', $entity->getId());
$query->execute();
}
static::$toDelete = [];
self::ensureKernelShutdown();
}
protected function tearDown(): void
{
self::ensureKernelShutdown();
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$news = new NewsItem();
$news->setContent('test content');
$news->setTitle('Title');
$news->setStartDate(new \DateTimeImmutable('yesterday'));
$em->persist($news);
$em->flush();
static::$toDelete[] = [NewsItem::class, $news];
self::ensureKernelShutdown();
yield [$news->getId()];
}
public function testList()
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', '/fr/news-items/history');
self::assertResponseIsSuccessful('Test that /fr/news-items history shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(int $newsItemId)
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/news-items/{$newsItemId}");
$this->assertResponseIsSuccessful('test that single news item page loads successfully');
}
}

View File

@@ -91,7 +91,7 @@ class JobWithReturn implements CronJobInterface
return 'with-data'; return 'with-data';
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
return ['data' => 'test']; return ['data' => 'test'];
} }

View File

@@ -175,7 +175,7 @@ class JobCanRun implements CronJobInterface
return $this->key; return $this->key;
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
return null; return null;
} }
@@ -193,7 +193,7 @@ class JobCannotRun implements CronJobInterface
return 'job-b'; return 'job-b';
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
return null; return null;
} }

View File

@@ -0,0 +1,108 @@
<?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 Repository;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Repository\NewsItemRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class NewsItemRepositoryTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
/**
* @var list<array{0: class-string, 1: NewsItem}>
*/
private array $toDelete = [];
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
protected function tearDown(): void
{
foreach ($this->toDelete as [$class, $entity]) {
$query = $this->entityManager->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class))
->setParameter('id', $entity->getId());
$query->execute();
}
$this->toDelete = [];
}
private function getNewsItemsRepository(ClockInterface $clock): NewsItemRepository
{
return new NewsItemRepository($this->entityManager, $clock);
}
public function testFindCurrentNews()
{
$clock = new MockClock($now = new \DateTimeImmutable('2023-01-10'));
$repository = $this->getNewsItemsRepository($clock);
$newsItem1 = new NewsItem();
$newsItem1->setTitle('This is a mock news item');
$newsItem1->setContent('We are testing that the repository returns the correct news items');
$newsItem1->setStartDate(new \DateTimeImmutable('2023-01-01'));
$newsItem1->setEndDate(new \DateTimeImmutable('2023-01-05'));
$newsItem2 = new NewsItem();
$newsItem2->setTitle('This is a mock news item');
$newsItem2->setContent('We are testing that the repository returns the correct news items');
$newsItem2->setStartDate(new \DateTimeImmutable('2023-01-01'));
$newsItem2->setEndDate($now->add(new \DateInterval('P1D')));
$newsItem3 = new NewsItem();
$newsItem3->setTitle('This is a mock news item');
$newsItem3->setContent('We are testing that the repository returns the correct news items');
$newsItem3->setStartDate(new \DateTimeImmutable('2033-11-03'));
$newsItem3->setEndDate(null);
$newsItem4 = new NewsItem();
$newsItem4->setTitle('This is a mock news item');
$newsItem4->setContent('We are testing that the repository returns the correct news items');
$newsItem4->setStartDate(new \DateTimeImmutable('2023-01-03'));
$newsItem4->setEndDate(null);
$this->entityManager->persist($newsItem1);
$this->entityManager->persist($newsItem2);
$this->entityManager->persist($newsItem3);
$this->entityManager->persist($newsItem4);
$this->entityManager->flush();
$this->toDelete = [
[NewsItem::class, $newsItem1],
[NewsItem::class, $newsItem2],
[NewsItem::class, $newsItem3],
[NewsItem::class, $newsItem4],
];
// Call the method to test
$result = $repository->findCurrentNews();
// Assertions
$this->assertCount(2, $result);
$this->assertInstanceOf(NewsItem::class, $result[0]);
$this->assertContains($newsItem2, $result);
$this->assertContains($newsItem4, $result);
}
}

View File

@@ -117,7 +117,7 @@ final class UserNormalizerTest extends TestCase
* *
* @throws ExceptionInterface * @throws ExceptionInterface
*/ */
public function testNormalize(User|null $user, mixed $format, mixed $context, mixed $expected) public function testNormalize(?User $user, mixed $format, mixed $context, mixed $expected)
{ {
$userRender = $this->prophesize(UserRender::class); $userRender = $this->prophesize(UserRender::class);
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : ''); $userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');

View File

@@ -21,7 +21,7 @@ class RoleScopeScopePresenceConstraint extends Constraint
public $messageNullRequired = 'The role "%role%" should not be associated with a scope.'; public $messageNullRequired = 'The role "%role%" should not be associated with a scope.';
public $messagePresenceRequired = 'The role "%role%" require to be associated with ' public $messagePresenceRequired = 'The role "%role%" require to be associated with '
.'a scope.'; .'a scope.';
public function getTargets() public function getTargets()
{ {

View File

@@ -10,6 +10,12 @@ servers:
components: components:
schemas: schemas:
Date:
type: object
properties:
datetime:
type: string
format: date-time
User: User:
type: object type: object
properties: properties:
@@ -131,6 +137,35 @@ components:
id: id:
type: integer type: integer
DashboardConfigItem:
type: object
properties:
id:
type: integer
type:
type: string
metadata:
type: object
userId:
type: integer
position:
type: string
NewsItem:
type: object
properties:
id:
type: integer
title:
type: string
content:
type: string
startDate:
$ref: "#/components/schemas/Date"
endDate:
$ref: "#/components/schemas/Date"
paths: paths:
/1.0/search.json: /1.0/search.json:
get: get:
@@ -842,4 +877,34 @@ paths:
$ref: '#/components/schemas/Workflow' $ref: '#/components/schemas/Workflow'
403: 403:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/dashboard-config-item.json:
get:
tags:
- dashboard config item
summary: Returns the dashboard configuration for the current user.
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardConfigItem'
403:
description: "Unauthorized"
/1.0/main/news/current.json:
get:
tags:
- news items
summary: Returns a list of news items which are valid
responses:
200:
description: "ok"
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/NewsItem'
403:
description: "Unauthorized"

View File

@@ -76,6 +76,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js'); encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js');
encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js'); encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js');
encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index'); encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index');
encore.addEntry('mod_news', __dirname + '/Resources/public/module/news/index.js');
// Vue entrypoints // Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');

View File

@@ -17,9 +17,6 @@ services:
- { name: console.command } - { name: console.command }
Chill\MainBundle\Command\LoadAndUpdateLanguagesCommand: Chill\MainBundle\Command\LoadAndUpdateLanguagesCommand:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$availableLanguages: '%chill_main.available_languages%'
tags: tags:
- { name: console.command } - { name: console.command }

View File

@@ -47,6 +47,8 @@ services:
Chill\MainBundle\Templating\Entity\AddressRender: ~ Chill\MainBundle\Templating\Entity\AddressRender: ~
Chill\MainBundle\Templating\Entity\NewsItemRender: ~
Chill\MainBundle\Templating\Entity\UserRender: ~ Chill\MainBundle\Templating\Entity\UserRender: ~
Chill\MainBundle\Templating\Listing\: Chill\MainBundle\Templating\Listing\:

View File

@@ -0,0 +1,55 @@
<?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\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create dashboard config item and news item.
*/
final class Version20231108141141 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create dashboard config item and news item';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_dashboard_config_item_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE chill_main_news_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_dashboard_config_item (id INT NOT NULL, user_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, position VARCHAR(255) NOT NULL, metadata JSONB DEFAULT \'{}\'::jsonb, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_CF59DFD6A76ED395 ON chill_main_dashboard_config_item (user_id)');
$this->addSql('CREATE TABLE chill_main_news (id INT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_96922AFB3174800F ON chill_main_news (createdBy_id)');
$this->addSql('CREATE INDEX IDX_96922AFB65FF1AEC ON chill_main_news (updatedBy_id)');
$this->addSql('COMMENT ON COLUMN chill_main_news.startDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_news.endDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_news.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_news.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_main_dashboard_config_item ADD CONSTRAINT FK_CF59DFD6A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_dashboard_config_item_id_seq CASCADE');
$this->addSql('DROP SEQUENCE chill_main_news_id_seq CASCADE');
$this->addSql('ALTER TABLE chill_main_dashboard_config_item DROP CONSTRAINT FK_CF59DFD6A76ED395');
$this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB3174800F');
$this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB65FF1AEC');
$this->addSql('DROP TABLE chill_main_dashboard_config_item');
$this->addSql('DROP TABLE chill_main_news');
}
}

View File

@@ -82,7 +82,6 @@ Comment: Commentaire
Comments: Commentaires Comments: Commentaires
Pinned comment: Commentaire épinglé Pinned comment: Commentaire épinglé
Any comment: Aucun commentaire Any comment: Aucun commentaire
Read more: Lire la suite
(more...): (suite...) (more...): (suite...)
# comment embeddable # comment embeddable
@@ -438,6 +437,16 @@ crud:
add_new: Ajouter un centre add_new: Ajouter un centre
title_new: Nouveau centre title_new: Nouveau centre
title_edit: Modifier un centre title_edit: Modifier un centre
news_item:
index:
title: Liste des actualités
add_new: Créer une nouvelle actualité
title_new: Nouvelle actualité
title_view: Voir l'actualité
title_edit: Modifier une actualité
title_delete: Supprimer une actualité
button_delete: Supprimer
confirm_message_delete: Êtes-vous sûr de vouloir supprimer l'actualité, "%as_string%" ?
No entities: Aucun élément No entities: Aucun élément
@@ -679,3 +688,20 @@ admin:
undefined: non défini undefined: non défini
user: Utilisateur user: Utilisateur
scope: Service scope: Service
dashboard:
title: Tableau de bord
news: Actualités
description: Configuration du tableau de bord
news:
noDate: Pas de date de fin
startDate: Date de début de publication
endDate: Date de fin de publication sur la page d'accueil
title: Historique des actualités
menu: Actualités
no_data: Aucune actualité
read_more: Lire la suite
show_details: Voir l'actualité

View File

@@ -39,7 +39,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
return 'accompanying-period-step-change'; return 'accompanying-period-step-change';
} }
public function run(array $lastExecutionData): array|null public function run(array $lastExecutionData): ?array
{ {
($this->requestor)(); ($this->requestor)();

Some files were not shown because too many files have changed in this diff Show More