Passed
Push — master ( 89fe32...89349c )
by
unknown
09:50
created

SessionRepository::getPastSessionsOfUserInUrl()   B

Complexity

Conditions 8
Paths 1

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 20
nc 1
nop 2
dl 0
loc 38
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Repository;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Entity\Course;
11
use Chamilo\CoreBundle\Entity\Session;
12
use Chamilo\CoreBundle\Entity\SessionRelCourse;
13
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
14
use Chamilo\CoreBundle\Entity\SessionRelUser;
15
use Chamilo\CoreBundle\Entity\User;
16
use Chamilo\CoreBundle\Settings\SettingsManager;
17
use DateTime;
18
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
19
use Doctrine\ORM\Query\Expr\Join;
20
use Doctrine\ORM\QueryBuilder;
21
use Doctrine\Persistence\ManagerRegistry;
22
use Exception;
23
24
/**
25
 * @author Julio Montoya <[email protected]>
26
 */
27
class SessionRepository extends ServiceEntityRepository
28
{
29
    public function __construct(
30
        ManagerRegistry $registry,
31
        private readonly SettingsManager $settingsManager,
32
    ) {
33
        parent::__construct($registry, Session::class);
34
    }
35
36
    public function create(): ?Session
37
    {
38
        return new Session();
39
    }
40
41
    public function update(Session $session): void
42
    {
43
        $this->getEntityManager()->persist($session);
44
        $this->getEntityManager()->flush();
45
    }
46
47
    /**
48
     * @return array<SessionRelUser>
49
     */
50
    public function getUsersByAccessUrl(Session $session, AccessUrl $url, array $relationTypeList = []): array
51
    {
52
        if (0 === $session->getUsers()->count()) {
53
            return [];
54
        }
55
56
        $qb = $this->addSessionRelUserFilterByUrl($session, $url);
57
        $qb->orderBy('sru.relationType');
58
59
        if ($relationTypeList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relationTypeList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
60
            $qb->andWhere(
61
                $qb->expr()->in('sru.relationType', $relationTypeList)
62
            );
63
        }
64
65
        return $qb->getQuery()->getResult();
66
    }
67
68
    public function getSessionsByUser(User $user, AccessUrl $url): QueryBuilder
69
    {
70
        $qb = $this->createQueryBuilder('s');
71
        $qb
72
            ->innerJoin('s.users', 'sru')
73
            ->leftJoin('s.urls', 'urls')
74
            ->where($qb->expr()->eq('sru.user', ':user'))
75
            ->andWhere($qb->expr()->eq('urls.url', ':url'))
76
            ->setParameters([
77
                'user' => $user,
78
                'url' => $url,
79
            ])
80
            ->orderBy('s.category', 'ASC') // by default sort by category, display date, title and position
81
            ->addOrderBy('s.displayStartDate', 'ASC')
82
            ->addOrderBy('s.title', 'ASC')
83
            ->addOrderBy('s.position', 'ASC')
84
        ;
85
86
        return $qb;
87
    }
88
89
    public function getSessionsByCourse(Course $course): array
90
    {
91
        $qb = $this->createQueryBuilder('s');
92
93
        return $qb
94
            ->innerJoin('s.courses', 'src')
95
            ->where($qb->expr()->eq('src.course', ':course'))
96
            ->setParameter('course', $course)
97
            ->getQuery()->getResult()
98
        ;
99
    }
100
101
    /**
102
     * @return array<int, Session>
103
     *
104
     * @throws Exception
105
     */
106
    public function getPastSessionsOfUserInUrl(User $user, AccessUrl $url): array
107
    {
108
        $sessions = $this->getSubscribedSessionsOfUserInUrl($user, $url);
109
        $now = new DateTime();
110
111
        $filterPastSessions = function (Session $session) use ($user, $now): bool {
112
            $userIsCoach = $session->hasCoach($user);
113
114
            // Duration sessions: past only for learners when expired (coaches never see them as past)
115
            if ($session->getDuration() > 0) {
116
                if ($userIsCoach) {
117
                    return false;
118
                }
119
120
                return $session->getDaysLeftByUser($user) < 0;
121
            }
122
123
            // Date-based sessions: prefer coach end date (if coach), else user end date, else session end date
124
            $subscription = $user->getSubscriptionToSession($session);
125
126
            $effectiveEndDate = null;
127
128
            if ($userIsCoach && $session->getCoachAccessEndDate()) {
129
                $effectiveEndDate = $session->getCoachAccessEndDate();
130
            } elseif ($subscription && $subscription->getAccessEndDate()) {
131
                $effectiveEndDate = $subscription->getAccessEndDate();
132
            } else {
133
                $effectiveEndDate = $session->getAccessEndDate();
134
            }
135
136
            if (!$effectiveEndDate) {
137
                return false;
138
            }
139
140
            return $now > $effectiveEndDate;
141
        };
142
143
        return array_values(array_filter($sessions, $filterPastSessions));
144
    }
145
146
    public function getCurrentSessionsOfUserInUrl(User $user, AccessUrl $url): QueryBuilder
147
    {
148
        $qb = $this->getSessionsByUser($user, $url)->distinct();
149
150
        $now = new DateTime();
151
152
        // Treat NULL duration as non-duration (same as 0)
153
        $nonDuration = $qb->expr()->orX(
154
            $qb->expr()->isNull('s.duration'),
155
            $qb->expr()->lte('s.duration', 0)
156
        );
157
158
        // Effective start date window:
159
        // - If user start date exists -> use it
160
        // - Else fallback to session start date
161
        // - If both NULL -> considered "open" on start side
162
        $startOk = $qb->expr()->orX(
163
            $qb->expr()->andX(
164
                $qb->expr()->isNull('sru.accessStartDate'),
165
                $qb->expr()->isNull('s.accessStartDate')
166
            ),
167
            $qb->expr()->lte('sru.accessStartDate', ':now'),
168
            $qb->expr()->andX(
169
                $qb->expr()->isNull('sru.accessStartDate'),
170
                $qb->expr()->lte('s.accessStartDate', ':now')
171
            )
172
        );
173
174
        // Fallback end date window (non-coach override case):
175
        $fallbackEndOk = $qb->expr()->orX(
176
            $qb->expr()->andX(
177
                $qb->expr()->isNull('sru.accessEndDate'),
178
                $qb->expr()->isNull('s.accessEndDate')
179
            ),
180
            $qb->expr()->gte('sru.accessEndDate', ':now'),
181
            $qb->expr()->andX(
182
                $qb->expr()->isNull('sru.accessEndDate'),
183
                $qb->expr()->gte('s.accessEndDate', ':now')
184
            )
185
        );
186
187
        // Coach override is active if relationType=coach AND session has coachAccessEndDate
188
        $coachOverrideActive = $qb->expr()->andX(
189
            $qb->expr()->eq('sru.relationType', ':coachRelationType'),
190
            $qb->expr()->isNotNull('s.coachAccessEndDate')
191
        );
192
193
        // Effective end date window:
194
        // - If coach override active -> use coachAccessEndDate
195
        // - Else fallback to (sru.accessEndDate || s.accessEndDate)
196
        $endOk = $qb->expr()->orX(
197
            $qb->expr()->andX(
198
                $coachOverrideActive,
199
                $qb->expr()->gte('s.coachAccessEndDate', ':now')
200
            ),
201
            $qb->expr()->andX(
202
                $qb->expr()->orX(
203
                    $qb->expr()->neq('sru.relationType', ':coachRelationType'),
204
                    $qb->expr()->isNull('s.coachAccessEndDate')
205
                ),
206
                $fallbackEndOk
207
            )
208
        );
209
210
        $dateBasedCurrent = $qb->expr()->andX($startOk, $endOk);
211
212
        return $qb
213
            ->andWhere(
214
                $qb->expr()->orX(
215
                // Duration sessions are candidates for "current" (provider filters expired ones via daysLeft)
216
                    $qb->expr()->gt('s.duration', 0),
217
218
                    // Date-based sessions must be inside the effective window
219
                    $qb->expr()->andX($nonDuration, $dateBasedCurrent)
220
                )
221
            )
222
            ->setParameter('now', $now)
223
            ->setParameter('coachRelationType', 3)
224
            // IMPORTANT: stable ordering for your scan pagination in the provider
225
            ->addOrderBy('s.id', 'ASC');
226
    }
227
228
    public function getUpcomingSessionsOfUserInUrl(User $user, AccessUrl $url): QueryBuilder
229
    {
230
        $qb = $this->getSessionsByUser($user, $url)->distinct();
231
        $now = new DateTime();
232
233
        $nonDuration = $qb->expr()->orX(
234
            $qb->expr()->isNull('s.duration'),
235
            $qb->expr()->lte('s.duration', 0)
236
        );
237
238
        // Effective start date > now:
239
        // - If sru.accessStartDate exists -> use it
240
        // - Else fallback to s.accessStartDate
241
        $upcomingStart = $qb->expr()->orX(
242
            $qb->expr()->gt('sru.accessStartDate', ':now'),
243
            $qb->expr()->andX(
244
                $qb->expr()->isNull('sru.accessStartDate'),
245
                $qb->expr()->gt('s.accessStartDate', ':now')
246
            )
247
        );
248
249
        return $qb
250
            ->andWhere($nonDuration)
251
            ->andWhere($upcomingStart)
252
            ->setParameter('now', $now)
253
            ->addOrderBy('sru.accessStartDate', 'ASC')
254
            ->addOrderBy('s.id', 'ASC');
255
    }
256
257
    public function addUserInCourse(int $relationType, User $user, Course $course, Session $session): void
258
    {
259
        if (!$user->isActive()) {
260
            throw new Exception('User not active');
261
        }
262
263
        if (!$session->hasCourse($course)) {
264
            $msg = \sprintf('Course %s is not subscribed to the session %s', $course->getTitle(), $session->getTitle());
265
266
            throw new Exception($msg);
267
        }
268
269
        if (!\in_array($relationType, Session::getRelationTypeList(), true)) {
270
            throw new Exception(\sprintf('Cannot handle relationType %s', $relationType));
271
        }
272
273
        $entityManager = $this->getEntityManager();
274
        $existingRecord = $entityManager->getRepository(SessionRelUser::class)->findOneBy([
275
            'session' => $session,
276
            'user' => $user,
277
            'relationType' => $relationType,
278
        ]);
279
280
        if ($existingRecord) {
281
            $entityManager->remove($existingRecord);
282
            $entityManager->flush();
283
        }
284
285
        switch ($relationType) {
286
            case Session::DRH:
287
                if ($user->isHRM()) {
288
                    $session->addUserInSession(Session::DRH, $user);
289
                }
290
291
                break;
292
293
            case Session::STUDENT:
294
                $session
295
                    ->addUserInSession(Session::STUDENT, $user)
296
                    ->addUserInCourse(Session::STUDENT, $user, $course)
297
                ;
298
299
                break;
300
301
            case Session::COURSE_COACH:
302
                if ($user->isTeacher()) {
303
                    $session
304
                        ->addUserInSession(Session::COURSE_COACH, $user)
305
                        ->addUserInCourse(
306
                            Session::COURSE_COACH,
307
                            $user,
308
                            $course
309
                        )
310
                    ;
311
                }
312
313
                break;
314
        }
315
316
        $entityManager->persist($session);
317
        $entityManager->flush();
318
    }
319
320
    /**
321
     * @return array<SessionRelCourse>
322
     */
323
    public function getSessionCoursesByStatusInUserSubscription(User $user, Session $session, int $relationType, ?AccessUrl $url = null): array
324
    {
325
        $qb = $this->getEntityManager()->createQueryBuilder();
326
327
        $qb->select('src')
328
            ->from(SessionRelCourse::class, 'src')
329
            ->innerJoin(
330
                SessionRelUser::class,
331
                'sru',
332
                Join::WITH,
333
                'src.session = sru.session'
334
            )
335
            ->innerJoin('src.session', 'session')
336
            ->where(
337
                $qb->expr()->eq('session', ':session')
338
            )
339
            ->andWhere(
340
                $qb->expr()->eq('sru.user', ':user')
341
            )
342
            ->andWhere(
343
                $qb->expr()->eq('sru.relationType', ':relation_type')
344
            )
345
        ;
346
347
        $parameters = [
348
            'session' => $session,
349
            'user' => $user,
350
            'relation_type' => $relationType,
351
        ];
352
353
        if ($url) {
354
            $qb->innerJoin('session.urls', 'urls')
355
                ->andWhere(
356
                    $qb->expr()->eq('urls.url', ':url')
357
                )
358
            ;
359
360
            $parameters['url'] = $url;
361
        }
362
363
        $qb->setParameters($parameters);
364
365
        return $qb->getQuery()->getResult();
366
    }
367
368
    /**
369
     * @return array<SessionRelCourse>
370
     */
371
    public function getSessionCoursesByStatusInCourseSubscription(User $user, Session $session, int $status, ?AccessUrl $url = null): array
372
    {
373
        $qb = $this->getEntityManager()->createQueryBuilder();
374
375
        $qb->select('src')
376
            ->from(SessionRelCourse::class, 'src')
377
            ->innerJoin(
378
                SessionRelCourseRelUser::class,
379
                'srcru',
380
                Join::WITH,
381
                'src.session = srcru.session AND src.course = srcru.course'
382
            )
383
            ->innerJoin('srcru.session', 'session')
384
            ->where(
385
                $qb->expr()->eq('session', ':session')
386
            )
387
            ->andWhere(
388
                $qb->expr()->eq('srcru.user', ':user')
389
            )
390
            ->andWhere(
391
                $qb->expr()->eq('srcru.status', ':status')
392
            )
393
        ;
394
395
        $parameters = [
396
            'session' => $session,
397
            'user' => $user,
398
            'status' => $status,
399
        ];
400
401
        if ($url) {
402
            $qb->innerJoin('session.urls', 'urls')
403
                ->andWhere(
404
                    $qb->expr()->eq('urls.url', ':url')
405
                )
406
            ;
407
408
            $parameters['url'] = $url;
409
        }
410
411
        $qb->setParameters($parameters);
412
413
        return $qb->getQuery()->getResult();
414
    }
415
416
    private function addSessionRelUserFilterByUrl(Session $session, AccessUrl $url): QueryBuilder
417
    {
418
        $qb = $this->getEntityManager()->createQueryBuilder();
419
        $qb
420
            ->select('sru')
421
            ->from(SessionRelUser::class, 'sru')
422
            ->innerJoin('sru.user', 'u')
423
            ->innerJoin('u.portals', 'p')
424
            ->andWhere('sru.session = :session AND p.url = :url')
425
            ->setParameters([
426
                'session' => $session,
427
                'url' => $url,
428
            ])
429
        ;
430
431
        return $qb;
432
    }
433
434
    public function getUserFollowedSessionsInAccessUrl(User $user, AccessUrl $url): QueryBuilder
435
    {
436
        $callback = fn (Session $session) => $session->getId();
437
438
        if ($user->isHRM()) {
439
            $idList = array_map($callback, $user->getDRHSessions());
440
        } elseif ($user->isTeacher() || COURSEMANAGER === $user->getStatus()) {
441
            $idListAsCoach = $user
442
                ->getSessionsByStatusInCourseSubscription(Session::COURSE_COACH)
443
                ->map($callback)
444
                ->getValues()
445
            ;
446
            $idListAsGeneralCoach = array_map($callback, $user->getSessionsAsGeneralCoach());
447
            $idList = array_merge($idListAsCoach, $idListAsGeneralCoach);
448
        } elseif ($user->isSessionAdmin()) {
449
            $idList = array_map($callback, $user->getSessionsAsAdmin());
450
        } else {
451
            $idList = array_map($callback, $user->getSessionsAsStudent());
452
        }
453
454
        $qb = $this->createQueryBuilder('s');
455
        $qb
456
            ->innerJoin('s.urls', 'u')
457
            ->where($qb->expr()->eq('u.url', $url->getId()))
458
            ->andWhere($qb->expr()->in('s.id', ':id_list'))
459
            ->setParameter('id_list', $idList)
460
        ;
461
462
        return $qb;
463
    }
464
465
    /**
466
     * @return array<int, Session>
467
     *
468
     * @throws Exception
469
     */
470
    public function getSubscribedSessionsOfUserInUrl(
471
        User $user,
472
        AccessUrl $url,
473
        bool $ignoreVisibilityForAdmins = false,
474
    ): array {
475
        $sessions = $this->getSessionsByUser($user, $url)->getQuery()->getResult();
476
477
        $filterSessions = function (Session $session) use ($user, $ignoreVisibilityForAdmins) {
478
            $visibility = $session->setAccessVisibilityByUser($user, $ignoreVisibilityForAdmins);
479
480
            if (Session::VISIBLE !== $visibility) {
481
                $closedOrHiddenCourses = $session->getClosedOrHiddenCourses();
482
483
                if ($closedOrHiddenCourses->count() === $session->getCourses()->count()) {
484
                    $visibility = Session::INVISIBLE;
485
                }
486
            }
487
488
            switch ($visibility) {
489
                case Session::READ_ONLY:
490
                case Session::VISIBLE:
491
                case Session::AVAILABLE:
492
                    break;
493
494
                case Session::INVISIBLE:
495
                    if (!$ignoreVisibilityForAdmins) {
496
                        return false;
497
                    }
498
            }
499
500
            return true;
501
        };
502
503
        return array_filter($sessions, $filterSessions);
504
    }
505
506
    /**
507
     * Finds a valid child session based on access dates and reinscription days.
508
     */
509
    public function findValidChildSession(Session $session): ?Session
510
    {
511
        $childSessions = $this->findChildSessions($session);
512
        $now = new DateTime();
513
514
        foreach ($childSessions as $childSession) {
515
            $startDate = $childSession->getAccessStartDate();
516
            $endDate = $childSession->getAccessEndDate();
517
            $daysToReinscription = $childSession->getDaysToReinscription();
518
519
            if (empty($daysToReinscription) || $daysToReinscription <= 0) {
520
                continue;
521
            }
522
523
            $adjustedEndDate = (clone $endDate)->modify('-'.$daysToReinscription.' days');
524
525
            if ($startDate <= $now && $adjustedEndDate >= $now) {
526
                return $childSession;
527
            }
528
        }
529
530
        return null;
531
    }
532
533
    /**
534
     * Finds a valid parent session based on access dates and reinscription days.
535
     */
536
    public function findValidParentSession(Session $session): ?Session
537
    {
538
        $parentSession = $this->findParentSession($session);
539
        if ($parentSession) {
540
            $now = new DateTime();
541
            $startDate = $parentSession->getAccessStartDate();
542
            $endDate = $parentSession->getAccessEndDate();
543
            $daysToReinscription = $parentSession->getDaysToReinscription();
544
545
            // Return null if days to reinscription is not set
546
            if (null === $daysToReinscription || '' === $daysToReinscription) {
547
                return null;
548
            }
549
550
            // Adjust the end date by days to reinscription
551
            $endDate = $endDate->modify('-'.$daysToReinscription.' days');
552
553
            // Check if the current date falls within the session's validity period
554
            if ($startDate <= $now && $endDate >= $now) {
555
                return $parentSession;
556
            }
557
        }
558
559
        return null;
560
    }
561
562
    /**
563
     * Finds child sessions based on the parent session.
564
     */
565
    public function findChildSessions(Session $parentSession): array
566
    {
567
        return $this->createQueryBuilder('s')
568
            ->where('s.parentId = :parentId')
569
            ->setParameter('parentId', $parentSession->getId())
570
            ->getQuery()
571
            ->getResult()
572
        ;
573
    }
574
575
    /**
576
     * Finds the parent session for a given session.
577
     */
578
    public function findParentSession(Session $session): ?Session
579
    {
580
        if ($session->getParentId()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session->getParentId() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
581
            return $this->find($session->getParentId());
582
        }
583
584
        return null;
585
    }
586
587
    /**
588
     * Find sessions without child and ready for repetition.
589
     *
590
     * @return Session[]
591
     */
592
    public function findSessionsWithoutChildAndReadyForRepetition()
593
    {
594
        $currentDate = new DateTime();
595
596
        $qb = $this->createQueryBuilder('s')
597
            ->where('s.daysToNewRepetition IS NOT NULL')
598
            ->andWhere('s.lastRepetition = :false')
599
            ->andWhere(':currentDate BETWEEN DATE_SUB(s.accessEndDate, s.daysToNewRepetition, \'DAY\') AND s.accessEndDate')
600
            ->andWhere('NOT EXISTS (
601
                SELECT 1
602
                FROM Chamilo\CoreBundle\Entity\Session child
603
                WHERE child.parentId = s.id
604
                AND child.accessEndDate >= :currentDate
605
            )')
606
            ->setParameter('false', false)
607
            ->setParameter('currentDate', $currentDate)
608
        ;
609
610
        return $qb->getQuery()->getResult();
611
    }
612
613
    public function countUsersBySession(int $sessionId, int $relationType = Session::STUDENT): int
614
    {
615
        $qb = $this->createQueryBuilder('s');
616
        $qb->select('COUNT(sru.id)')
617
            ->innerJoin('s.users', 'sru')
618
            ->where('s.id = :sessionId')
619
            ->andWhere('sru.relationType = :relationType')
620
            ->setParameter('sessionId', $sessionId)
621
            ->setParameter('relationType', $relationType)
622
        ;
623
624
        return (int) $qb->getQuery()->getSingleScalarResult();
625
    }
626
}
627