update some queries in the interface to take into account history of user's scope and user's job

This commit is contained in:
Julien Fastré 2023-10-11 15:14:27 +02:00
parent 363785b779
commit 978db5a5c5
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
14 changed files with 210 additions and 57 deletions

View File

@ -122,9 +122,30 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
->leftJoin('a.user', 'activity_u')
->andWhere(
$qb->expr()->orX(
'creator.userJob IN (:jobs)',
'activity_u.userJob IN (:jobs)',
'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))'
$qb->expr()->exists(
sprintf(
"SELECT 1 FROM %s ujh_creator WHERE ujh_creator.user = a.createdBy "
. "AND ujh_creator.job IN (:jobs) AND a.createdAt > ujh_creator.startDate "
. "AND (ujh_creator.endDate IS NULL or ujh_creator.endDate > a.date)",
User\UserJobHistory::class
)
),
$qb->expr()->exists(
sprintf(
"SELECT 1 FROM %s ujh_u WHERE ujh_u.user = a.user "
. "AND ujh_u.job IN (:jobs) AND a.createdAt > ujh_u.startDate "
. "AND (ujh_u.endDate IS NULL or ujh_u.endDate > a.date)",
User\UserJobHistory::class
)
),
$qb->expr()->exists(
sprintf(
"SELECT 1 FROM %s ujh_users WHERE ujh_users.user MEMBER OF a.users "
. "AND ujh_users.job IN (:jobs) AND a.createdAt > ujh_users.startDate "
. "AND (ujh_users.endDate IS NULL or ujh_users.endDate > a.date)",
User\UserJobHistory::class
)
),
)
)
->setParameter('jobs', $jobs);
@ -175,13 +196,14 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array
{
$in = $this->em->createQueryBuilder();
$in->select('IDENTITY(u.userJob)')
->from(User::class, 'u')
$in->select('IDENTITY(u.job)')
->distinct()
->from(User\UserJobHistory::class, 'u')
->join(
Activity::class,
'a',
Join::WITH,
'a.createdBy = u OR a.user = u OR u MEMBER OF a.users'
'a.createdBy = u.user OR a.user = u.user OR u.user MEMBER OF a.users AND a.date >= u.startDate ANd (u.endDate IS NULL or u.endDate > a.date)'
);
if ($associated instanceof Person) {

View File

@ -59,10 +59,10 @@ final readonly class UserExportController
'label',
'mainCenter_id' ,
'mainCenter_name',
'mainScope_id', ///
'mainScope_name', ///
'userJob_id', ///
'userJob_name', ///
'mainScope_id',
'mainScope_name',
'userJob_id',
'userJob_name',
'currentLocation_id',
'currentLocation_name',
'mainLocation_id',

View File

@ -13,7 +13,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\AbstractQuery;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
@ -27,7 +28,11 @@ final readonly class UserRepository implements UserRepositoryInterface
{
private EntityRepository $repository;
public function __construct(private EntityManagerInterface $entityManager)
private const FIELDS = ['id', 'email', 'enabled', 'civility_id', 'civility_abbreviation', 'civility_name', 'label', 'mainCenter_id',
'mainCenter_name', 'mainScope_id', 'mainScope_name', 'userJob_id', 'userJob_name', 'currentLocation_id', 'currentLocation_name',
'mainLocation_id', 'mainLocation_name'];
public function __construct(private EntityManagerInterface $entityManager, private Connection $connection)
{
$this->repository = $entityManager->getRepository(User::class);
}
@ -75,48 +80,58 @@ final readonly class UserRepository implements UserRepositoryInterface
}
/**
* @param string $lang
* @throws Exception
*/
public function findAllAsArray(string $lang): iterable
{
$dql = sprintf(<<<'DQL'
$sql = sprintf(<<<'SQL'
SELECT
u.id AS id,
u.id,
u.username AS username,
u.email,
u.email AS email,
u.enabled,
IDENTITY(u.civility) AS civility_id,
JSON_EXTRACT(civility.abbreviation, :lang) AS civility_abbreviation,
JSON_EXTRACT(civility.name, :lang) AS civility_name,
u.civility_id,
civility.abbreviation->>:lang AS civility_abbreviation,
civility.name->>:lang AS civility_name,
u.label,
mainCenter.id AS mainCenter_id,
mainCenter.name AS mainCenter_name,
IDENTITY(u.mainScope) AS mainScope_id,
JSON_EXTRACT(mainScope.name, :lang) AS mainScope_name,
IDENTITY(u.userJob) AS userJob_id,
JSON_EXTRACT(userJob.label, :lang) AS userJob_name,
mainScope.id AS mainScope_id,
mainScope.name->>:lang AS mainScope_name,
userJob.id AS userJob_id,
userJob.label->>:lang AS userJob_name,
currentLocation.id AS currentLocation_id,
currentLocation.name AS currentLocation_name,
mainLocation.id AS mainLocation_id,
mainLocation.name AS mainLocation_name,
u.absenceStart
FROM Chill\MainBundle\Entity\User u
LEFT JOIN u.civility civility
LEFT JOIN u.currentLocation currentLocation
LEFT JOIN u.mainLocation mainLocation
LEFT JOIN u.mainCenter mainCenter
LEFT JOIN u.mainScope mainScope
LEFT JOIN u.userJob userJob
ORDER BY u.label
DQL); /// mainScope userJob
FROM users u
LEFT JOIN chill_main_civility civility ON u.civility_id = civility.id
LEFT JOIN centers mainCenter ON u.maincenter_id = mainCenter.id
LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id
LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id
LEFT JOIN chill_main_user_scope_history userScopeHistory ON u.id = userScopeHistory.user_id
LEFT JOIN scopes mainScope ON userScopeHistory.scope_id = mainScope.id
LEFT JOIN chill_main_location currentLocation ON u.currentlocation_id = currentLocation.id
LEFT JOIN chill_main_location mainLocation ON u.mainlocation_id = mainLocation.id
WHERE
tstzrange(userScopeHistory.startdate, userScopeHistory.enddate) @> NOW()
AND
tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW()
SQL);
$query = $this->entityManager->createQuery($dql)
->setHydrationMode(AbstractQuery::HYDRATE_ARRAY)
->setParameter('lang', $lang)
;
$query = $this->connection->prepare($sql);
foreach ($query->toIterable() as $u) {
yield $u;
foreach ($query->executeQuery(['lang' => $lang])->iterateAssociative() as $u) {
$converted = [];
foreach (self::FIELDS as $f) {
$converted[$f] = $u[strtolower($f)];
}
$converted['absenceStart'] = null !== $u['absencestart'] ? new \DateTimeImmutable($u['absencestart']) : null;
/** @phpstan-ignore-next-line phpstan does not take into account that all required keys will be present */
yield $converted;
}
}

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Service\EntityInfo;
use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
class ViewEntityInfoManager
{
@ -21,6 +22,7 @@ class ViewEntityInfoManager
*/
private readonly iterable $vienEntityInfoProviders,
private readonly Connection $connection,
private readonly LoggerInterface $logger,
) {}
public function synchronizeOnDB(): void
@ -28,6 +30,8 @@ class ViewEntityInfoManager
$this->connection->transactional(function (Connection $conn): void {
foreach ($this->vienEntityInfoProviders as $viewProvider) {
foreach ($this->createOrReplaceViewSQL($viewProvider, $viewProvider->getViewName()) as $sql) {
$this->logger->debug("Will execute create view sql", ['sql' => $sql]);
$this->logger->debug($sql);
$conn->executeQuery($sql);
}
}
@ -41,7 +45,7 @@ class ViewEntityInfoManager
{
return [
"DROP VIEW IF EXISTS {$viewName}",
sprintf("CREATE VIEW {$viewName} AS %s", $viewProvider->getViewQuery())
sprintf("CREATE OR REPLACE VIEW {$viewName} AS %s", $viewProvider->getViewQuery())
];
}
}

View File

@ -157,7 +157,6 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @var Collection<int, AccompanyingPeriodWorkReferrerHistory>
* @ORM\OneToMany(targetEntity=AccompanyingPeriodWorkReferrerHistory::class, cascade={"persist", "remove"}, mappedBy="accompanyingPeriodWork", orphanRemoval=true)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_referrer")
*/
private Collection $referrersHistory;

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@ -91,7 +92,9 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval')),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'work.referrers')
$qb->expr()->exists(
'SELECT 1 FROM ' . AccompanyingPeriodWork::class . ' subw JOIN subw.referrersHistory subw_ref_history WHERE subw.id = work.id AND subw_ref_history.user = :user'
)
)
)
)

View File

@ -132,10 +132,10 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor
// set limit and offset
$sql .= " ORDER BY
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
startdate DESC,
enddate DESC,
id DESC";
CASE WHEN w.enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
w.startdate DESC,
w.enddate DESC,
w.id DESC";
$sql .= " LIMIT :limit OFFSET :offset";
@ -239,7 +239,9 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor
$qb->expr()->lte('w.startDate', ':until'),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'w.referrers')
$qb->expr()->exists(
'SELECT 1 FROM ' . AccompanyingPeriodWork::class . ' subw JOIN subw.referrersHistory subw_ref_history WHERE subw.id = w.id AND subw_ref_history.user = :user and subw.ref_history.endDate IS NULL'
)
)
)
)

View File

@ -54,7 +54,7 @@ class AccompanyingPeriodWorkEndQueryPartForAccompanyingPeriodInfo implements Acc
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id';
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON w.id = cpapwr.accompanyingperiodwork_id AND daterange(cpapwr.startDate, cpapwr.endDate) @> w.endDate';
}
public function getWhereClause(): string

View File

@ -11,10 +11,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkEvaluationWarningDateQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
class AccompanyingPeriodWorkEvaluationCreationQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
@ -33,12 +34,12 @@ class AccompanyingPeriodWorkEvaluationWarningDateQueryPartForAccompanyingPeriodI
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
return 'e.createdBy_id';
}
public function getDateTimeColumn(): string
{
return 'e.maxDate';
return 'e.createdAt';
}
public function getMetadataColumn(): string
@ -48,18 +49,18 @@ class AccompanyingPeriodWorkEvaluationWarningDateQueryPartForAccompanyingPeriodI
public function getDiscriminator(): string
{
return 'accompanying_period_work_evaluation_max';
return 'accompanying_period_work_evaluation_creation';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id AND daterange(cpapwr.startDate, cpapwr.endDate) @> e.startDate';
}
public function getWhereClause(): string
{
return 'e.maxDate IS NOT NULL';
return 'e.createdAt IS NOT NULL';
}
}

View File

@ -56,7 +56,7 @@ class AccompanyingPeriodWorkEvaluationMaxQueryPartForAccompanyingPeriodInfo impl
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id AND daterange(cpapwr.startDate, cpapwr.endDate) @> e.maxDate';
}
public function getWhereClause(): string

View File

@ -56,11 +56,11 @@ class AccompanyingPeriodWorkEvaluationStartQueryPartForAccompanyingPeriodInfo im
{
return 'chill_person_accompanying_period_work_evaluation e
JOIN chill_person_accompanying_period_work cpapw ON cpapw.id = e.accompanyingperiodwork_id
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id';
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr ON cpapw.id = cpapwr.accompanyingperiodwork_id AND daterange(cpapwr.startDate, cpapwr.endDate) @> e.startDate';
}
public function getWhereClause(): string
{
return '';
return 'e.startDate IS NOT NULL';
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoQueryPart;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
class AccompanyingPeriodWorkNewReferrerQueryPartForAccompanyingPeriodInfo implements AccompanyingPeriodInfoUnionQueryPartInterface
{
public function getAccompanyingPeriodIdColumn(): string
{
return 'w.accompanyingperiod_id';
}
public function getRelatedEntityColumn(): string
{
return AccompanyingPeriodWork::class;
}
public function getRelatedEntityIdColumn(): string
{
return 'w.id';
}
public function getUserIdColumn(): string
{
return 'cpapwr.user_id';
}
public function getDateTimeColumn(): string
{
return 'cpapwr.startDate';
}
public function getMetadataColumn(): string
{
return "'{}'::jsonb";
}
public function getDiscriminator(): string
{
return 'accompanying_period_work_referrer_new';
}
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work w
JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id';
}
public function getWhereClause(): string
{
return '';
}
}

View File

@ -54,7 +54,7 @@ class AccompanyingPeriodWorkStartQueryPartForAccompanyingPeriodInfo implements A
public function getFromStatement(): string
{
return 'chill_person_accompanying_period_work w
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id';
LEFT JOIN chill_person_accompanying_period_work_referrer cpapwr on w.id = cpapwr.accompanyingperiodwork_id AND daterange(cpapwr.startDate, cpapwr.endDate) @> w.startDate';
}
public function getWhereClause(): string

View File

@ -0,0 +1,43 @@
<?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\PersonBundle\Tests\Repository;
use Chill\MainBundle\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
class UserRepositoryTest extends KernelTestCase
{
private UserRepository $userRepository;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->userRepository = self::$container->get(UserRepository::class);
}
public function testFindAllAsArray(): void
{
$userIterator = $this->userRepository->findAllAsArray('fr');
self::assertIsIterable($userIterator);
$i = 0;
foreach ($userIterator as $u) {
self::assertIsArray($u);
}
self::assertGreaterThan(0, $i);
}
}