Refactoring subscribed session list for user - refs BT#21745 #5560

According to session visibility by the user and considering sessions with access with a duration time
pull/5576/head
Angel Fernando Quiroz Campos 1 year ago
parent 3f53feaf0f
commit 1a15aa0b09
  1. 4
      assets/vue/components/session/SessionCardSimple.vue
  2. 1
      assets/vue/constants/entity/session.js
  3. 100
      src/CoreBundle/Entity/Session.php
  4. 202
      src/CoreBundle/Repository/SessionRepository.php
  5. 9
      src/CoreBundle/Security/Authorization/Voter/SessionVoter.php
  6. 18
      src/CoreBundle/Serializer/Normalizer/SessionNormalizer.php
  7. 6
      src/CoreBundle/State/UserSessionSubscriptionsStateProvider.php

@ -1,6 +1,6 @@
<script setup>
import CourseCard from "../course/CourseCard.vue"
import { SESSION_VISIBILITY_INVISIBLE } from "../../constants/entity/session"
import { SESSION_VISIBILITY_LIST_ONLY } from "../../constants/entity/session"
const props = defineProps({
session: {
@ -13,7 +13,7 @@ const courses = props.session.courses
? props.session.courses.map((sesionRelCourse) => ({ ...sesionRelCourse.course, _id: sesionRelCourse.course.id }))
: []
const enableAccess = props.session.accessVisibility !== SESSION_VISIBILITY_INVISIBLE
const enableAccess = props.session.accessVisibility !== SESSION_VISIBILITY_LIST_ONLY
</script>
<template>

@ -3,6 +3,7 @@ export const SESSION_VISIBILITY_VISIBLE = 1
export const SESSION_VISIBILITY_READ_ONLY = 2
export const SESSION_VISIBILITY_INVISIBLE = 3
export const SESSION_VISIBILITY_AVAILABLE = 4
export const SESSION_VISIBILITY_LIST_ONLY = 5
// Session user roles
export const SESSION_STUDENT = 0

@ -26,6 +26,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\ORM\Mapping as ORM;
use LogicException;
use Stringable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
@ -113,6 +114,7 @@ class Session implements ResourceWithAccessUrlInterface, Stringable
public const VISIBLE = 2;
public const INVISIBLE = 3;
public const AVAILABLE = 4;
public const LIST_ONLY = 5;
public const STUDENT = 0;
public const DRH = 1;
@ -357,6 +359,9 @@ class Session implements ResourceWithAccessUrlInterface, Stringable
#[ORM\JoinColumn(name: 'image_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
protected ?Asset $image = null;
#[Groups(['user_subscriptions:sessions', 'session:read', 'session:item:read'])]
private int $accessVisibility = 0;
public function __construct()
{
$this->skills = new ArrayCollection();
@ -1292,6 +1297,28 @@ class Session implements ResourceWithAccessUrlInterface, Stringable
return $totalDuration > $currentTime;
}
public function getDaysLeftByUser(User $user): int
{
$userSessionSubscription = $user->getSubscriptionToSession($this);
$duration = $this->duration;
if ($userSessionSubscription) {
$duration += $userSessionSubscription->getDuration();
}
$courseAccess = $user->getFirstAccessToSession($this);
if (!$courseAccess) {
return $duration;
}
$endDateInSeconds = $courseAccess->getLoginCourseDate()->getTimestamp() + $duration * 24 * 60 * 60;
$currentTime = time();
return (int) round(($endDateInSeconds - $currentTime) / 60 / 60 / 24);
}
private function getAccessVisibilityByDuration(User $user): int
{
// Session duration per student.
@ -1331,60 +1358,57 @@ class Session implements ResourceWithAccessUrlInterface, Stringable
private function getAcessVisibilityByDates(User $user): int
{
$now = new DateTime();
$visibility = $this->getVisibility();
// If start date was set.
if ($this->getAccessStartDate()) {
$visibility = $now > $this->getAccessStartDate() ? self::AVAILABLE : self::INVISIBLE;
}
$userIsCoach = $this->hasCoach($user);
// If the end date was set.
if ($this->getAccessEndDate()) {
// Only if date_start said that it was ok
if (self::AVAILABLE === $visibility) {
$visibility = $now < $this->getAccessEndDate()
? self::AVAILABLE // Date still available
: $this->getVisibility(); // Session ends
}
}
$sessionEndDate = $userIsCoach && $this->coachAccessEndDate
? $this->coachAccessEndDate
: $this->accessEndDate;
// If I'm a coach the visibility can change in my favor depending in the coach dates.
$isCoach = $this->hasCoach($user);
if ($isCoach) {
// Test start date.
if ($this->getCoachAccessStartDate()) {
$visibility = $this->getCoachAccessStartDate() < $now ? self::AVAILABLE : self::INVISIBLE;
}
if (!$userIsCoach && $this->accessStartDate && $now < $this->accessStartDate) {
return self::LIST_ONLY;
}
// Test end date.
if ($this->getCoachAccessEndDate()) {
if (self::AVAILABLE === $visibility) {
$visibility = $this->getCoachAccessEndDate() >= $now ? self::AVAILABLE : $this->getVisibility();
}
}
if ($sessionEndDate) {
return $now <= $sessionEndDate ? self::AVAILABLE : $this->visibility;
}
return $visibility;
return self::AVAILABLE;
}
public function checkAccessVisibilityByUser(User $user): int
public function setAccessVisibilityByUser(User $user): int
{
if ($user->isAdmin() || $user->isSuperAdmin()) {
return self::AVAILABLE;
}
if (null === $this->getAccessStartDate() && null === $this->getAccessEndDate()) {
$this->accessVisibility = self::AVAILABLE;
} elseif (!$this->getAccessStartDate() && !$this->getAccessEndDate()) {
// I don't care the session visibility.
return $this->getAccessVisibilityByDuration($user);
$this->accessVisibility = $this->getAccessVisibilityByDuration($user);
} else {
$this->accessVisibility = $this->getAcessVisibilityByDates($user);
}
return $this->getAcessVisibilityByDates($user);
return $this->accessVisibility;
}
#[Groups(['user_subscriptions:sessions', 'session:read', 'session:item:read'])]
public function getAccessVisibility(): int
{
return 0;
if (0 === $this->accessVisibility) {
throw new LogicException('Access visibility by user is not set');
}
return $this->accessVisibility;
}
public function getClosedOrHiddenCourses(): Collection
{
$closedVisibilities = [
Course::CLOSED,
Course::HIDDEN,
];
return $this->courses->filter(fn (SessionRelCourse $sessionRelCourse) => \in_array(
$sessionRelCourse->getCourse()->getVisibility(),
$closedVisibilities
));
}
}

@ -13,8 +13,8 @@ use Chamilo\CoreBundle\Entity\SessionRelCourse;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\CoreBundle\Entity\SessionRelUser;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Settings\SettingsManager;
use DateTime;
use DateTimeZone;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
@ -26,8 +26,10 @@ use Exception;
*/
class SessionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
public function __construct(
ManagerRegistry $registry,
private readonly SettingsManager $settingsManager,
) {
parent::__construct($registry, Session::class);
}
@ -85,22 +87,35 @@ class SessionRepository extends ServiceEntityRepository
*
* @throws Exception
*/
public function getPastSessionsWithDatesForUser(User $user, AccessUrl $url): array
public function getPastSessionsOfUserInUrl(User $user, AccessUrl $url): array
{
$now = new DateTime('now', new DateTimeZone('UTC'));
$sessions = $this->getSubscribedSessionsOfUserInUrl($user, $url);
$qb = $this->getSessionsByUser($user, $url);
$qb
->andWhere(
$qb->expr()->andX(
$qb->expr()->isNotNull('s.accessEndDate'),
$qb->expr()->lt('s.accessEndDate', ':now')
)
)
->setParameter('now', $now)
;
$filterPastSessions = function (Session $session) use ($user) {
$now = new DateTime();
return $qb->getQuery()->getResult();
// Check if the session has a duration
if ($session->getDuration() > 0) {
$daysLeft = $session->getDaysLeftByUser($user);
return $daysLeft < 0;
}
// Get the appropriate end date based on whether the user is a coach
$sessionEndDate = $session->hasCoach($user) && $session->getCoachAccessEndDate()
? $session->getCoachAccessEndDate()
: $session->getAccessEndDate();
// If there's no end date, the session is not considered past
if (!$sessionEndDate) {
return false;
}
// Check if the current date is after the end date
return $now > $sessionEndDate;
};
return array_filter($sessions, $filterPastSessions);
}
/**
@ -108,44 +123,45 @@ class SessionRepository extends ServiceEntityRepository
*
* @throws Exception
*/
public function getCurrentSessionsWithDatesForUser(User $user, AccessUrl $url): array
public function getCurrentSessionsOfUserInUrl(User $user, AccessUrl $url): array
{
$now = new DateTime('now', new DateTimeZone('UTC'));
$sessions = $this->getSubscribedSessionsOfUserInUrl($user, $url);
$qb = $this->getSessionsByUser($user, $url);
$qb
->andWhere(
$qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->isNull('s.accessStartDate'),
$qb->expr()->isNull('s.accessEndDate'),
$qb->expr()->orX(
$qb->expr()->eq('s.duration', 0),
$qb->expr()->isNull('s.duration')
)
),
$qb->expr()->andX(
$qb->expr()->isNotNull('s.accessStartDate'),
$qb->expr()->isNull('s.accessEndDate'),
$qb->expr()->lte('s.accessStartDate', ':now')
),
$qb->expr()->andX(
$qb->expr()->isNotNull('s.accessStartDate'),
$qb->expr()->isNotNull('s.accessEndDate'),
$qb->expr()->lte('s.accessStartDate', ':now'),
$qb->expr()->gte('s.accessEndDate', ':now')
),
$qb->expr()->andX(
$qb->expr()->isNull('s.accessStartDate'),
$qb->expr()->isNotNull('s.accessEndDate'),
$qb->expr()->gte('s.accessEndDate', ':now')
)
)
)
->setParameter('now', $now)
;
$filterCurrentSessions = function (Session $session) use ($user) {
// Check if session has a duration
if ($session->getDuration() > 0) {
$daysLeft = $session->getDaysLeftByUser($user);
$firstAccess = $user->getFirstAccessToSession($session);
return $qb->getQuery()->getResult();
return $daysLeft >= 0 && $daysLeft <= $session->getDuration() && $firstAccess;
}
// Determine if the user is a coach
$userIsCoach = $session->hasCoach($user);
// Determine the start date based on whether the user is a coach
$sessionStartDate = $userIsCoach && $session->getCoachAccessStartDate()
? $session->getCoachAccessStartDate()
: $session->getAccessStartDate();
// If there is no start date, consider the session current
if (!$sessionStartDate) {
return true;
}
// Get the current date and time
$now = new DateTime();
// Determine the end date based on whether the user is a coach
$sessionEndDate = $userIsCoach && $session->getCoachAccessEndDate()
? $session->getCoachAccessEndDate()
: $session->getAccessEndDate();
// Check if the current date is within the start and end dates
return $now >= $sessionStartDate && (!$sessionEndDate || $now <= $sessionEndDate);
};
return array_filter($sessions, $filterCurrentSessions);
}
/**
@ -153,22 +169,39 @@ class SessionRepository extends ServiceEntityRepository
*
* @throws Exception
*/
public function getUpcomingSessionsWithDatesForUser(User $user, AccessUrl $url): array
public function getUpcomingSessionsOfUserInUrl(User $user, AccessUrl $url): array
{
$now = new DateTime('now', new DateTimeZone('UTC'));
$sessions = $this->getSubscribedSessionsOfUserInUrl($user, $url);
$qb = $this->getSessionsByUser($user, $url);
$qb
->andWhere(
$qb->expr()->andX(
$qb->expr()->isNotNull('s.accessStartDate'),
$qb->expr()->gt('s.accessStartDate', ':now')
)
)
->setParameter('now', $now)
;
$filterUpcomingSessions = function (Session $session) use ($user) {
$now = new DateTime();
return $qb->getQuery()->getResult();
// Check if the session has a duration
if ($session->getDuration() > 0) {
$daysLeft = $session->getDaysLeftByUser($user);
$firstAccess = $user->getFirstAccessToSession($session);
return $daysLeft >= $session->getDuration() && !$firstAccess;
}
// Determine if the user is a coach
$userIsCoach = $session->hasCoach($user);
// Get the appropriate start date based on whether the user is a coach
$sessionStartDate = $userIsCoach && $session->getCoachAccessStartDate()
? $session->getCoachAccessStartDate()
: $session->getAccessStartDate();
// If there's no start date, the session is not considered future
if (!$sessionStartDate) {
return false;
}
// Check if the current date is before the start date
return $now < $sessionStartDate;
};
return array_filter($sessions, $filterUpcomingSessions);
}
public function addUserInCourse(int $relationType, User $user, Course $course, Session $session): void
@ -363,4 +396,45 @@ class SessionRepository extends ServiceEntityRepository
return $qb;
}
/**
* @return array<int, Session>
*
* @throws Exception
*/
public function getSubscribedSessionsOfUserInUrl(
User $user,
AccessUrl $url,
bool $ignoreVisibilityForAdmin = false,
): array {
$sessions = $this->getSessionsByUser($user, $url)->getQuery()->getResult();
$filterSessions = function (Session $session) use ($user, $ignoreVisibilityForAdmin) {
$visibility = $session->setAccessVisibilityByUser($user);
if (Session::VISIBLE !== $visibility) {
$closedOrHiddenCourses = $session->getClosedOrHiddenCourses();
if ($closedOrHiddenCourses->count() === $session->getCourses()->count()) {
$visibility = Session::INVISIBLE;
}
}
switch ($visibility) {
case Session::READ_ONLY:
case Session::VISIBLE:
case Session::AVAILABLE:
break;
case Session::INVISIBLE:
if (!$ignoreVisibilityForAdmin) {
return false;
}
}
return true;
};
return array_filter($sessions, $filterSessions);
}
}

@ -92,11 +92,14 @@ class SessionVoter extends Voter
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_STUDENT);
}
if (Session::INVISIBLE !== $session->checkAccessVisibilityByUser($user)) {
return true;
if (\in_array(
$session->setAccessVisibilityByUser($user),
[Session::INVISIBLE, Session::LIST_ONLY]
)) {
return false;
}
return false;
return true;
case self::EDIT:
case self::DELETE:

@ -8,6 +8,7 @@ namespace Chamilo\CoreBundle\Serializer\Normalizer;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
use LogicException;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -28,7 +29,15 @@ class SessionNormalizer implements NormalizerInterface, NormalizerAwareInterface
$data = $this->normalizer->normalize($object, $format, $context);
$data['accessVisibility'] = $this->getSessionAccessVisiblity($object);
\assert($object instanceof Session);
try {
$object->getAccessVisibility();
} catch (LogicException) {
$data['accessVisibility'] = $object->setAccessVisibilityByUser(
$this->userHelper->getCurrent()
);
}
return $data;
}
@ -46,11 +55,4 @@ class SessionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
return [Session::class => false];
}
private function getSessionAccessVisiblity(Session $session): int
{
return $session->checkAccessVisibilityByUser(
$this->userHelper->getCurrent()
);
}
}

@ -50,9 +50,9 @@ class UserSessionSubscriptionsStateProvider implements ProviderInterface
}
return match ($operation->getName()) {
'user_session_subscriptions_past' => $this->sessionRepository->getPastSessionsWithDatesForUser($user, $url),
'user_session_subscriptions_current' => $this->sessionRepository->getCurrentSessionsWithDatesForUser($user, $url),
'user_session_subscriptions_upcoming' => $this->sessionRepository->getUpcomingSessionsWithDatesForUser($user, $url),
'user_session_subscriptions_past' => $this->sessionRepository->getPastSessionsOfUserInUrl($user, $url),
'user_session_subscriptions_current' => $this->sessionRepository->getCurrentSessionsOfUserInUrl($user, $url),
'user_session_subscriptions_upcoming' => $this->sessionRepository->getUpcomingSessionsOfUserInUrl($user, $url),
};
}
}

Loading…
Cancel
Save