Merge pull request #5576 from AngelFQC/BT21745

Refactoring subscribed session list for user
pull/5604/head
Nicolas Ducoulombier 5 months ago committed by GitHub
commit 6a105fb6f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      assets/vue/components/session/SessionCardSimple.vue
  2. 24
      assets/vue/composables/my_course_list/myCourseListSessions.js
  3. 1
      assets/vue/constants/entity/session.js
  4. 108
      src/CoreBundle/Entity/Session.php
  5. 201
      src/CoreBundle/Repository/SessionRepository.php
  6. 15
      src/CoreBundle/Security/Authorization/Voter/SessionVoter.php
  7. 20
      src/CoreBundle/Serializer/Normalizer/SessionNormalizer.php
  8. 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 { useSessionCard } from "../../composables/my_course_list/myCourseListSessions"
const props = defineProps({
session: {
@ -9,11 +9,7 @@ const props = defineProps({
},
})
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 { courses, isEnabled } = useSessionCard(props.session)
</script>
<template>
@ -26,7 +22,7 @@ const enableAccess = props.session.accessVisibility !== SESSION_VISIBILITY_INVIS
:session="session"
:course="course"
:session-id="session.id"
:disabled="!enableAccess"
:disabled="!isEnabled"
/>
</div>
</template>

@ -0,0 +1,24 @@
import { SESSION_VISIBILITY_LIST_ONLY } from "../../constants/entity/session"
import { useSecurityStore } from "../../store/securityStore"
/**
* @param {Object} session
* @returns {{courses: Object[], isEnabled: boolean}}
*/
export function useSessionCard(session) {
const securityStore = useSecurityStore()
/**
* @type {Object[]}
*/
const courses = session.courses
? session.courses.map((sesionRelCourse) => ({ ...sesionRelCourse.course, _id: sesionRelCourse.course.id }))
: []
const isEnabled = session.accessVisibility !== SESSION_VISIBILITY_LIST_ONLY || securityStore.isAdmin
return {
courses,
isEnabled,
}
}

@ -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,10 +1297,36 @@ 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.
if ($this->getDuration() > 0) {
if ($this->hasCoach($user)) {
return self::AVAILABLE;
}
$duration = $this->getDuration() * 24 * 60 * 60;
$courseAccess = $user->getFirstAccessToSession($this);
@ -1314,7 +1345,7 @@ class Session implements ResourceWithAccessUrlInterface, Stringable
$totalDuration = $firstAccess + $duration + $userDuration;
return $totalDuration > $currentTime ? self::AVAILABLE : self::READ_ONLY;
return $totalDuration > $currentTime ? self::AVAILABLE : $this->visibility;
}
return self::AVAILABLE;
@ -1331,60 +1362,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;
}
// 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
}
}
$userIsCoach = $this->hasCoach($user);
// If I'm a coach the visibility can change in my favor depending in the coach dates.
$isCoach = $this->hasCoach($user);
$sessionEndDate = $userIsCoach && $this->coachAccessEndDate
? $this->coachAccessEndDate
: $this->accessEndDate;
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, bool $ignoreVisibilityForAdmins = true): int
{
if ($user->isAdmin() || $user->isSuperAdmin()) {
return self::AVAILABLE;
}
if (null === $this->getAccessStartDate() && null === $this->getAccessEndDate()) {
if (($user->isAdmin() || $user->isSuperAdmin()) && $ignoreVisibilityForAdmins) {
$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,38 @@ 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();
// Determine if the user is a coach
$userIsCoach = $session->hasCoach($user);
return $qb->getQuery()->getResult();
// Check if the session has a duration
if ($session->getDuration() > 0) {
$daysLeft = $session->getDaysLeftByUser($user);
$session->setTitle($session->getTitle().'<-'.$daysLeft);
return $daysLeft < 0 && !$userIsCoach;
}
// Get the appropriate end date based on whether the user is a coach
$sessionEndDate = $userIsCoach && $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 +126,44 @@ 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) {
// Determine if the user is a coach
$userIsCoach = $session->hasCoach($user);
return $qb->getQuery()->getResult();
// Check if session has a duration
if ($session->getDuration() > 0) {
$daysLeft = $session->getDaysLeftByUser($user);
return $daysLeft >= 0 || $userIsCoach;
}
// 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 +171,36 @@ 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();
// All session with access by duration call be either current or past
if ($session->getDuration() > 0) {
return false;
}
// 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 +395,45 @@ class SessionRepository extends ServiceEntityRepository
return $qb;
}
/**
* @return array<int, Session>
*
* @throws Exception
*/
public function getSubscribedSessionsOfUserInUrl(
User $user,
AccessUrl $url,
bool $ignoreVisibilityForAdmins = false,
): array {
$sessions = $this->getSessionsByUser($user, $url)->getQuery()->getResult();
$filterSessions = function (Session $session) use ($user, $ignoreVisibilityForAdmins) {
$visibility = $session->setAccessVisibilityByUser($user, $ignoreVisibilityForAdmins);
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 (!$ignoreVisibilityForAdmins) {
return false;
}
}
return true;
};
return array_filter($sessions, $filterSessions);
}
}

@ -84,15 +84,22 @@ class SessionVoter extends Voter
$userIsStudent = $session->hasUserInCourse($user, $currentCourse, Session::STUDENT);
}
if ($userIsGeneralCoach) {
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_TEACHER);
} elseif ($userIsCourseCoach) { // Course-Coach access.
$visibilityForUser = $session->setAccessVisibilityByUser($user);
if ($userIsStudent && Session::LIST_ONLY == $visibilityForUser) {
return false;
}
if ($userIsGeneralCoach || $userIsCourseCoach) {
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_TEACHER);
} elseif ($userIsStudent) { // Student access.
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_STUDENT);
}
if (Session::INVISIBLE !== $session->checkAccessVisibilityByUser($user)) {
if (
($userIsGeneralCoach || $userIsCourseCoach || $userIsStudent)
&& $visibilityForUser != Session::INVISIBLE
) {
return true;
}

@ -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;
@ -26,11 +27,17 @@ class SessionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
$context[self::ALREADY_CALLED] = true;
$data = $this->normalizer->normalize($object, $format, $context);
\assert($object instanceof Session);
$data['accessVisibility'] = $this->getSessionAccessVisiblity($object);
try {
$object->getAccessVisibility();
} catch (LogicException) {
$object->setAccessVisibilityByUser(
$this->userHelper->getCurrent()
);
}
return $data;
return $this->normalizer->normalize($object, $format, $context);
}
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
@ -46,11 +53,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