From ba4e25bc9a76d7949c66338116ffcf51c97e3424 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Mon, 30 Sep 2024 14:16:36 -0500 Subject: [PATCH 01/11] Internal: Add session replication, reinscription logic, and child session hierarchy for course expiration - refs BT#22057 --- public/main/inc/ajax/model.ajax.php | 4 +- public/main/inc/lib/sessionmanager.lib.php | 45 +++- public/main/session/session_list.php | 16 +- .../Command/ReinscriptionCheckCommand.php | 185 ++++++++++++++++ .../Command/SessionRepetitionCommand.php | 205 ++++++++++++++++++ src/CoreBundle/Entity/Session.php | 56 +++++ .../Schema/V200/Version20240928003000.php | 83 +++++++ .../Repository/SessionRepository.php | 103 +++++++++ src/CourseBundle/Entity/CLp.php | 14 ++ src/CourseBundle/Repository/CLpRepository.php | 29 +++ 10 files changed, 726 insertions(+), 14 deletions(-) create mode 100644 src/CoreBundle/Command/ReinscriptionCheckCommand.php create mode 100644 src/CoreBundle/Command/SessionRepetitionCommand.php create mode 100644 src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php diff --git a/public/main/inc/ajax/model.ajax.php b/public/main/inc/ajax/model.ajax.php index 8d2d283c09d..7b223dcc534 100644 --- a/public/main/inc/ajax/model.ajax.php +++ b/public/main/inc/ajax/model.ajax.php @@ -677,7 +677,7 @@ function getWhereClause($col, $oper, $val) $count = ExerciseLib::get_count_exam_results( $exerciseId, $whereCondition, - '', + $courseId, false, true, $status @@ -839,6 +839,7 @@ function getWhereClause($col, $oper, $val) ['where' => $whereCondition, 'extra' => $extra_fields] ); break; + case 'replication': case 'custom': case 'simple': $count = SessionManager::getSessionsForAdmin( @@ -1981,6 +1982,7 @@ function getWhereClause($col, $oper, $val) break; case 'custom': case 'simple': + case 'replication': $result = SessionManager::getSessionsForAdmin( api_get_user_id(), [ diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index a34a9f2b12f..8e6f3f9c380 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -521,6 +521,10 @@ public static function getSessionsForAdmin( } $select .= ', status'; + if ('replication' === $listType) { + $select .= ', parent_id'; + } + if (isset($options['order'])) { $isMakingOrder = 0 === strpos($options['order'], 'category_name'); } @@ -652,6 +656,11 @@ public static function getSessionsForAdmin( ) )"; break; + case 'replication': + $formatted = false; + $query .= "AND s.days_to_new_repetition IS NOT NULL + AND (SELECT COUNT(id) FROM session AS child WHERE child.parent_id = s.id) <= 1"; + break; } $query .= $order; @@ -666,6 +675,23 @@ public static function getSessionsForAdmin( $session['users'] = Database::fetch_assoc($result)['nbr']; } } + + if ('replication' === $listType) { + $formattedSessions = []; + foreach ($sessions as $session) { + $formattedSessions[] = $session; + if (isset($session['id'])) { + $childSessions = array_filter($sessions, fn($s) => isset($s['parent_id']) && $s['parent_id'] === $session['id']); + foreach ($childSessions as $childSession) { + $childSession['title'] = '-- ' . $childSession['title']; + $formattedSessions[] = $childSession; + } + } + } + + return $formattedSessions; + } + if ('all' === $listType) { if ($getCount) { return $sessions[0]['total_rows']; @@ -8699,7 +8725,7 @@ public static function getGridColumns( ]; break; - + case 'replication': case 'custom': $columns = [ '#', @@ -8718,7 +8744,7 @@ public static function getGridColumns( [ 'name' => 'title', 'index' => 's.title', - 'width' => '160', + 'width' => '260px', 'align' => 'left', 'search' => 'true', 'searchoptions' => ['sopt' => $operators], @@ -9709,9 +9735,9 @@ public static function getDefaultSessionTab() } /** - * @return array + * @return string */ - public static function getSessionListTabs($listType) + public static function getSessionListTabs($listType): string { $tabs = [ [ @@ -9730,10 +9756,10 @@ public static function getSessionListTabs($listType) 'content' => get_lang('Custom list'), 'url' => api_get_path(WEB_CODE_PATH).'session/session_list.php?list_type=custom', ], - /*[ - 'content' => get_lang('Complete'), - 'url' => api_get_path(WEB_CODE_PATH).'session/session_list_simple.php?list_type=complete', - ],*/ + [ + 'content' => get_lang('Replication'), + 'url' => api_get_path(WEB_CODE_PATH).'session/session_list.php?list_type=replication', + ], ]; $default = null; switch ($listType) { @@ -9749,6 +9775,9 @@ public static function getSessionListTabs($listType) case 'custom': $default = 4; break; + case 'replication': + $default = 5; + break; } return Display::tabsOnlyLink($tabs, $default); diff --git a/public/main/session/session_list.php b/public/main/session/session_list.php index 31a8cae46ca..aec7e2f8f6a 100644 --- a/public/main/session/session_list.php +++ b/public/main/session/session_list.php @@ -96,11 +96,17 @@ }); '; -// jqgrid will use this URL to do the selects -if (!empty($courseId)) { - $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&course_id='.$courseId; -} else { - $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions'; +switch ($listType) { + case 'replication': + $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&list_type=replication'; + break; + default: + if (!empty($courseId)) { + $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&course_id='.$courseId; + } else { + $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions'; + } + break; } if (isset($_REQUEST['keyword'])) { diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php new file mode 100644 index 00000000000..81ca7f9d569 --- /dev/null +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -0,0 +1,185 @@ +lpRepository = $lpRepository; + $this->sessionRepository = $sessionRepository; + $this->entityManager = $entityManager; + } + + protected function configure(): void + { + $this + ->setDescription('Checks for users whose course completions have expired and reinscribe them into new sessions if needed.') + ->addOption( + 'debug', + null, + InputOption::VALUE_NONE, + 'If set, debug messages will be shown.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $debug = $input->getOption('debug'); + + // 1. Find all lessons with "validity_in_days" > 0 + $learningPaths = $this->lpRepository->findWithValidity(); + + /* @var CLp $lp */ + foreach ($learningPaths as $lp) { + $validityDays = $lp->getValidityInDays(); + $sessionId = $this->lpRepository->getLpSessionId($lp->getIid()); + + if (!$sessionId) { + if ($debug) { + $output->writeln('Session ID not found for Learning Path ID: ' . $lp->getIid()); + } + continue; + } + + // 2. Get the session of the lesson + $session = $this->sessionRepository->find($sessionId); + if (!$session) { + if ($debug) { + $output->writeln('Session not found for ID: ' . $sessionId); + } + continue; + } + + // Process only if the session is not the last repetition + if ($session->getLastRepetition()) { + if ($debug) { + $output->writeln('Session ' . $session->getId() . ' is the last repetition. Skipping...'); + } + continue; + } + + // 3. Find users who completed the lesson and whose validity has expired + $expiredUsers = $this->findExpiredCompletions($lp, $validityDays); + + if (count($expiredUsers) === 0) { + if ($debug) { + $output->writeln('No expired users found for Learning Path ID: ' . $lp->getIid()); + } + continue; + } + + foreach ($expiredUsers as $user) { + if ($debug) { + $output->writeln('User ' . $user->getUser()->getId() . ' has expired completion for LP ' . $lp->getIid()); + } + + // 4. Find the last valid child session + $validChildSession = $this->sessionRepository->findValidChildSession($session); + + if ($validChildSession) { + // Reinscribe user in the valid child session + $this->enrollUserInSession($user->getUser(), $validChildSession); + if ($debug) { + $output->writeln('Reinscribed user ' . $user->getUser()->getId() . ' into child session ' . $validChildSession->getId()); + } + } else { + // 5. If no valid child session, find the valid parent session + $validParentSession = $this->sessionRepository->findValidParentSession($session); + if ($validParentSession) { + // Reinscribe user in the valid parent session + $this->enrollUserInSession($user->getUser(), $validParentSession); + if ($debug) { + $output->writeln('Reinscribed user ' . $user->getUser()->getId() . ' into parent session ' . $validParentSession->getId()); + } + } else { + if ($debug) { + $output->writeln('No valid parent or child session found for user ' . $user->getUser()->getId()); + } + } + } + } + } + + return Command::SUCCESS; + } + + /** + * Find users with expired completion based on "validity_in_days". + */ + private function findExpiredCompletions($lp, $validityDays) + { + $now = new \DateTime(); + $expirationDate = (clone $now)->modify('-' . $validityDays . ' days'); + + // Find users with 100% completion and whose last access date (start_time) is older than 'validity_in_days' + return $this->entityManager->getRepository(CLpView::class) + ->createQueryBuilder('v') + ->innerJoin('Chamilo\CourseBundle\Entity\CLpItemView', 'iv', 'WITH', 'iv.view = v') + ->where('v.lp = :lp') + ->andWhere('v.progress = 100') + ->andWhere('iv.startTime < :expirationDate') + ->setParameter('lp', $lp) + ->setParameter('expirationDate', $expirationDate->getTimestamp()) + ->getQuery() + ->getResult(); + } + + /** + * Enrolls a user into a session. + */ + private function enrollUserInSession($user, $session): void + { + // First, check if the user is already enrolled in the session + $existingSubscription = $this->findUserSubscriptionInSession($user, $session); + + if ($existingSubscription) { + // Remove existing subscription before re-enrolling the user + $session->removeUserSubscription($existingSubscription); + $this->entityManager->persist($session); + $this->entityManager->flush(); + } + + // Add the user into the session as a student + $session->addUserInSession(Session::STUDENT, $user); + + // Save the changes to the database + $this->entityManager->persist($session); + $this->entityManager->flush(); + } + + private function findUserSubscriptionInSession($user, $session) + { + return $this->entityManager->getRepository(SessionRelUser::class) + ->findOneBy([ + 'user' => $user, + 'session' => $session, + ]); + } +} diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php new file mode 100644 index 00000000000..bb8e9d5db59 --- /dev/null +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -0,0 +1,205 @@ +sessionRepository = $sessionRepository; + $this->entityManager = $entityManager; + $this->mailer = $mailer; + $this->translator = $translator; + } + + protected function configure(): void + { + $this + ->setDescription('Automatically duplicates sessions that meet the repetition criteria.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $debug = $input->getOption('debug'); + + // Find sessions that meet the repetition criteria + $sessions = $this->sessionRepository->findSessionsWithoutChildAndReadyForRepetition(); + + if ($debug) { + $output->writeln(sprintf('Found %d session(s) ready for repetition.', count($sessions))); + } + + foreach ($sessions as $session) { + if ($debug) { + $output->writeln(sprintf('Processing session: %d', $session->getId())); + } + + // Duplicate session + $newSession = $this->duplicateSession($session, $debug, $output); + + // Notify general coach of the new session + $this->notifyGeneralCoach($newSession, $debug, $output); + + $output->writeln('Created new session: ' . $newSession->getId() . ' from session: ' . $session->getId()); + } + + return Command::SUCCESS; + } + + /** + * Duplicates a session and creates a new session with adjusted dates. + */ + private function duplicateSession(Session $session, bool $debug, OutputInterface $output): Session + { + // Calculate new session dates based on the duration of the original session + $duration = $session->getAccessEndDate()->diff($session->getAccessStartDate()); + $newStartDate = (clone $session->getAccessEndDate())->modify('+1 day'); + $newEndDate = (clone $newStartDate)->add($duration); + + if ($debug) { + $output->writeln(sprintf('Duplicating session %d. New start date: %s, New end date: %s', + $session->getId(), + $newStartDate->format('Y-m-d H:i:s'), + $newEndDate->format('Y-m-d H:i:s') + )); + } + + // Create a new session with the same details as the original session + $newSession = new Session(); + $newSession + ->setTitle($session->getTitle() . ' (Repetition ' . $session->getId() . ')') + ->setAccessStartDate($newStartDate) + ->setAccessEndDate($newEndDate) + ->setDisplayStartDate($newStartDate) + ->setDisplayEndDate($newEndDate) + ->setCoachAccessStartDate($newStartDate) + ->setCoachAccessEndDate($newEndDate) + ->setVisibility($session->getVisibility()) + ->setDuration($session->getDuration()) + ->setDescription($session->getDescription() ?? '') + ->setShowDescription($session->getShowDescription()) + ->setCategory($session->getCategory()) + ->setPromotion($session->getPromotion()) + ->setLastRepetition(false); + + // Copy the AccessUrls from the original session + $accessUrls = $session->getUrls(); + + if ($accessUrls->isEmpty()) { + // Handle the case where the session does not have any AccessUrl + if ($debug) { + $output->writeln('No AccessUrl found for session ' . $session->getId() . '. Assigning default AccessUrl.'); + } + + // Retrieve or create a default AccessUrl (you need to adjust this based on your system's needs) + $defaultAccessUrl = $this->getDefaultAccessUrl(); + $newSession->addAccessUrl($defaultAccessUrl); + } else { + foreach ($accessUrls as $accessUrl) { + $newSession->addAccessUrl($accessUrl->getUrl()); + } + } + + // Save the new session + $this->entityManager->persist($newSession); + $this->entityManager->flush(); + + if ($debug) { + $output->writeln(sprintf('New session %d created successfully.', $newSession->getId())); + } + + return $newSession; + } + + /** + * Retrieves or creates a default AccessUrl for sessions. + */ + private function getDefaultAccessUrl() + { + return $this->entityManager->getRepository(AccessUrl::class)->findOneBy([]); + } + + + /** + * Notifies the general coach of the session about the new repetition. + */ + private function notifyGeneralCoach(Session $newSession, bool $debug, OutputInterface $output): void + { + $generalCoach = $newSession->getGeneralCoaches()->first(); + if ($generalCoach) { + $message = sprintf( + 'A new repetition of the session "%s" has been created. Please review the details: %s', + $newSession->getTitle(), + $this->generateSessionSummaryLink($newSession) + ); + + if ($debug) { + $output->writeln(sprintf('Notifying coach (ID: %d) for session %d', $generalCoach->getId(), $newSession->getId())); + } + + // Send message to the general coach + $this->sendMessage($generalCoach->getEmail(), $message); + + if ($debug) { + $output->writeln('Notification sent.'); + } + } else { + if ($debug) { + $output->writeln('No general coach found for session ' . $newSession->getId()); + } + } + } + + /** + * Sends an email message to a user. + */ + private function sendMessage(string $recipientEmail, string $message): void + { + $subject = $this->translator->trans('New Session Repetition Created'); + + $email = (new Email()) + ->from('no-reply@yourdomain.com') + ->to($recipientEmail) + ->subject($subject) + ->html('

' . $message . '

'); + + $this->mailer->send($email); + } + + /** + * Generates a link to the session summary page. + */ + private function generateSessionSummaryLink(Session $session): string + { + return '/main/session/resume_session.php?id_session=' . $session->getId(); + } +} diff --git a/src/CoreBundle/Entity/Session.php b/src/CoreBundle/Entity/Session.php index cc879119f26..84bd14b2ea9 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -374,6 +374,18 @@ class Session implements ResourceWithAccessUrlInterface, Stringable #[Groups(['user_subscriptions:sessions', 'session:read', 'session:item:read'])] private int $accessVisibility = 0; + #[ORM\Column(name: 'parent_id', type: 'integer', nullable: true)] + protected ?int $parentId = null; + + #[ORM\Column(name: 'days_to_reinscription', type: 'integer', nullable: true)] + protected ?int $daysToReinscription = null; + + #[ORM\Column(name: 'last_repetition', type: 'boolean', nullable: false, options: ['default' => false])] + protected bool $lastRepetition = false; + + #[ORM\Column(name: 'days_to_new_repetition', type: 'integer', nullable: true)] + protected ?int $daysToNewRepetition = null; + public function __construct() { $this->skills = new ArrayCollection(); @@ -1448,4 +1460,48 @@ public function getClosedOrHiddenCourses(): Collection $closedVisibilities )); } + + public function getParentId(): ?int + { + return $this->parentId; + } + + public function setParentId(?int $parentId): self + { + $this->parentId = $parentId; + return $this; + } + + public function getDaysToReinscription(): ?int + { + return $this->daysToReinscription; + } + + public function setDaysToReinscription(?int $daysToReinscription): self + { + $this->daysToReinscription = $daysToReinscription; + return $this; + } + + public function getLastRepetition(): bool + { + return $this->lastRepetition; + } + + public function setLastRepetition(bool $lastRepetition): self + { + $this->lastRepetition = $lastRepetition; + return $this; + } + + public function getDaysToNewRepetition(): ?int + { + return $this->daysToNewRepetition; + } + + public function setDaysToNewRepetition(?int $daysToNewRepetition): self + { + $this->daysToNewRepetition = $daysToNewRepetition; + return $this; + } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php new file mode 100644 index 00000000000..7731ab6cbb6 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php @@ -0,0 +1,83 @@ +connection->createSchemaManager(); + + // Add fields to the 'session' table + if ($schemaManager->tablesExist('session')) { + $sessionTable = $schemaManager->listTableColumns('session'); + + if (!isset($sessionTable['parent_id'])) { + $this->addSql("ALTER TABLE session ADD parent_id INT DEFAULT NULL"); + } + if (!isset($sessionTable['days_to_reinscription'])) { + $this->addSql("ALTER TABLE session ADD days_to_reinscription INT DEFAULT NULL"); + } + if (!isset($sessionTable['last_repetition'])) { + $this->addSql("ALTER TABLE session ADD last_repetition TINYINT(1) DEFAULT 0 NOT NULL"); + } + if (!isset($sessionTable['days_to_new_repetition'])) { + $this->addSql("ALTER TABLE session ADD days_to_new_repetition INT DEFAULT NULL"); + } + } + + // Add the field to the 'c_lp' (Learnpath) table + if ($schemaManager->tablesExist('c_lp')) { + $clpTable = $schemaManager->listTableColumns('c_lp'); + + if (!isset($clpTable['validity_in_days'])) { + $this->addSql("ALTER TABLE c_lp ADD validity_in_days INT DEFAULT NULL"); + } + } + } + + public function down(Schema $schema): void + { + $schemaManager = $this->connection->createSchemaManager(); + + // Revert changes in the 'session' table + if ($schemaManager->tablesExist('session')) { + $sessionTable = $schemaManager->listTableColumns('session'); + + if (isset($sessionTable['parent_id'])) { + $this->addSql("ALTER TABLE session DROP COLUMN parent_id"); + } + if (isset($sessionTable['days_to_reinscription'])) { + $this->addSql("ALTER TABLE session DROP COLUMN days_to_reinscription"); + } + if (isset($sessionTable['last_repetition'])) { + $this->addSql("ALTER TABLE session DROP COLUMN last_repetition"); + } + if (isset($sessionTable['days_to_new_repetition'])) { + $this->addSql("ALTER TABLE session DROP COLUMN days_to_new_repetition"); + } + } + + // Revert changes in the 'c_lp' table + if ($schemaManager->tablesExist('c_lp')) { + $clpTable = $schemaManager->listTableColumns('c_lp'); + + if (isset($clpTable['validity_in_days'])) { + $this->addSql("ALTER TABLE c_lp DROP COLUMN validity_in_days"); + } + } + } +} diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index d4a9595ed30..06022d52cda 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -463,4 +463,107 @@ public function getSubscribedSessionsOfUserInUrl( return array_filter($sessions, $filterSessions); } + + /** + * Finds a valid child session based on access dates and reinscription days. + * + * @param Session $session + * @return Session|null + */ + public function findValidChildSession(Session $session): ?Session + { + $childSessions = $this->findChildSessions($session); + foreach ($childSessions as $childSession) { + $now = new \DateTime(); + $startDate = $childSession->getAccessStartDate(); + $endDate = $childSession->getAccessEndDate(); + $daysToReinscription = $childSession->getDaysToReinscription(); + + // Skip if days to reinscription is not set + if ($daysToReinscription === null || $daysToReinscription === '') { + continue; + } + + // Adjust the end date by days to reinscription + $endDate = $endDate->modify('-' . $daysToReinscription . ' days'); + + // Check if the current date falls within the session's validity period + if ($startDate <= $now && $endDate >= $now) { + return $childSession; + } + } + return null; + } + + /** + * Finds a valid parent session based on access dates and reinscription days. + */ + public function findValidParentSession(Session $session): ?Session + { + $parentSession = $this->findParentSession($session); + if ($parentSession) { + $now = new \DateTime(); + $startDate = $parentSession->getAccessStartDate(); + $endDate = $parentSession->getAccessEndDate(); + $daysToReinscription = $parentSession->getDaysToReinscription(); + + // Return null if days to reinscription is not set + if ($daysToReinscription === null || $daysToReinscription === '') { + return null; + } + + // Adjust the end date by days to reinscription + $endDate = $endDate->modify('-' . $daysToReinscription . ' days'); + + // Check if the current date falls within the session's validity period + if ($startDate <= $now && $endDate >= $now) { + return $parentSession; + } + } + return null; + } + + /** + * Finds child sessions based on the parent session. + */ + public function findChildSessions(Session $parentSession): array + { + return $this->createQueryBuilder('s') + ->where('s.parentId = :parentId') + ->setParameter('parentId', $parentSession->getId()) + ->getQuery() + ->getResult(); + } + + /** + * Finds the parent session for a given session. + */ + public function findParentSession(Session $session): ?Session + { + if ($session->getParentId()) { + return $this->find($session->getParentId()); + } + + return null; + } + + /** + * Find sessions without child and ready for repetition. + * + * @return Session[] + */ + public function findSessionsWithoutChildAndReadyForRepetition() + { + $currentDate = new \DateTime(); + + $qb = $this->createQueryBuilder('s') + ->where('s.parentId IS NULL') + ->andWhere('s.daysToNewRepetition IS NOT NULL') + ->andWhere('s.lastRepetition = :false') + ->andWhere(':currentDate BETWEEN DATE_SUB(s.accessEndDate, s.daysToNewRepetition, \'DAY\') AND s.accessEndDate') + ->setParameter('false', false) + ->setParameter('currentDate', $currentDate); + + return $qb->getQuery()->getResult(); + } } diff --git a/src/CourseBundle/Entity/CLp.php b/src/CourseBundle/Entity/CLp.php index a2cb14bb17b..7c75fa2ac9c 100644 --- a/src/CourseBundle/Entity/CLp.php +++ b/src/CourseBundle/Entity/CLp.php @@ -152,6 +152,9 @@ class CLp extends AbstractResource implements ResourceInterface, ResourceShowCou #[ORM\Column(name: 'duration', type: 'integer', nullable: true)] protected ?int $duration = null; + #[ORM\Column(name: 'validity_in_days', type: 'integer', nullable: true)] + protected ?int $validityInDays = null; + public function __construct() { $now = new DateTime(); @@ -617,6 +620,17 @@ public function setDuration(?int $duration): self return $this; } + public function getValidityInDays(): ?int + { + return $this->validityInDays; + } + + public function setValidityInDays(?int $validityInDays): self + { + $this->validityInDays = $validityInDays; + return $this; + } + public function getResourceIdentifier(): int|Uuid { return $this->getIid(); diff --git a/src/CourseBundle/Repository/CLpRepository.php b/src/CourseBundle/Repository/CLpRepository.php index 60de76bebdd..13cca448c38 100644 --- a/src/CourseBundle/Repository/CLpRepository.php +++ b/src/CourseBundle/Repository/CLpRepository.php @@ -103,4 +103,33 @@ protected function addNotDeletedQueryBuilder(?QueryBuilder $qb = null): QueryBui return $qb; } + + public function getLpSessionId(int $lpId): ?int + { + $lp = $this->find($lpId); + + if (!$lp) { + return null; + } + + $resourceNode = $lp->getResourceNode(); + if ($resourceNode) { + $link = $resourceNode->getResourceLinks()->first(); + + if ($link && $link->getSession()) { + + return (int) $link->getSession()->getId(); + } + } + + return null; + } + + public function findWithValidity(): array + { + return $this->createQueryBuilder('lp') + ->where('lp.validityInDays > 0') + ->getQuery() + ->getResult(); + } } From 7f6e7aebdb4d68d7a8aa785e5a9c41000b438992 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Wed, 9 Oct 2024 18:22:29 -0500 Subject: [PATCH 02/11] Session: Add settings and interface fields for session and lesson functionality - refs BT#22057 --- public/main/inc/lib/sessionmanager.lib.php | 123 +++++++++++++----- public/main/lp/lp_add.php | 17 +++ public/main/lp/lp_edit.php | 17 +++ public/main/session/session_add.php | 21 ++- public/main/session/session_edit.php | 15 ++- .../Command/SessionRepetitionCommand.php | 4 +- .../DataFixtures/SettingsCurrentFixtures.php | 10 ++ src/CoreBundle/Entity/Session.php | 2 +- .../Settings/SessionSettingsSchema.php | 4 + 9 files changed, 170 insertions(+), 43 deletions(-) diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index 8e6f3f9c380..5aae927eeff 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -150,6 +150,10 @@ public static function create_session( array $coachesId, $sessionCategoryId, $visibility = 1, + $parentId = null, + $daysBeforeFinishingForReinscription = null, + $lastRepetition = false, + $daysBeforeFinishingToCreateNewRepetition = null, $fixSessionNameIfExists = false, $duration = null, $description = null, @@ -187,25 +191,17 @@ public static function create_session( $endDate = Database::escape_string($endDate); if (empty($name)) { - $msg = get_lang('A title is required for the session'); - - return $msg; + return get_lang('A title is required for the session'); } elseif (!empty($startDate) && !api_is_valid_date($startDate, 'Y-m-d H:i') && !api_is_valid_date($startDate, 'Y-m-d H:i:s') ) { - $msg = get_lang('Invalid start date was given.'); - - return $msg; + return get_lang('Invalid start date was given.'); } elseif (!empty($endDate) && !api_is_valid_date($endDate, 'Y-m-d H:i') && !api_is_valid_date($endDate, 'Y-m-d H:i:s') ) { - $msg = get_lang('Invalid end date was given.'); - - return $msg; + return get_lang('Invalid end date was given.'); } elseif (!empty($startDate) && !empty($endDate) && $startDate >= $endDate) { - $msg = get_lang('The first date should be before the end date'); - - return $msg; + return get_lang('The first date should be before the end date'); } else { $ready_to_create = false; if ($fixSessionNameIfExists) { @@ -213,16 +209,12 @@ public static function create_session( if ($name) { $ready_to_create = true; } else { - $msg = get_lang('Session title already exists'); - - return $msg; + return get_lang('Session title already exists'); } } else { $rs = Database::query("SELECT 1 FROM $tbl_session WHERE title='".$name."'"); if (Database::num_rows($rs)) { - $msg = get_lang('Session title already exists'); - - return $msg; + return get_lang('Session title already exists'); } $ready_to_create = true; } @@ -237,7 +229,10 @@ public static function create_session( ->setDescription($description) ->setShowDescription(1 === $showDescription) ->setSendSubscriptionNotification((bool) $sendSubscriptionNotification) - ; + ->setParentId($parentId) + ->setDaysToReinscription($daysBeforeFinishingForReinscription) + ->setLastRepetition($lastRepetition) + ->setDaysToNewRepetition($daysBeforeFinishingToCreateNewRepetition); foreach ($coachesId as $coachId) { $session->addGeneralCoach(api_get_user_entity($coachId)); @@ -286,18 +281,6 @@ public static function create_session( $extraFields['item_id'] = $session_id; $sessionFieldValue = new ExtraFieldValue('session'); $sessionFieldValue->saveFieldValues($extraFields); - /* - Sends a message to the user_id = 1 - - $user_info = api_get_user_info(1); - $complete_name = $user_info['firstname'].' '.$user_info['lastname']; - $subject = api_get_setting('siteName').' - '.get_lang('A new session has been created'); - $message = get_lang('A new session has been created')."
".get_lang('Session name').' : '.$name; - api_mail_html($complete_name, $user_info['email'], $subject, $message); - * - */ - // Adding to the correct URL - //UrlManager::add_session_to_url($session_id, $accessUrlId); // add event to system log $user_id = api_get_user_id(); @@ -1816,7 +1799,11 @@ public static function edit_session( $extraFields = [], $sessionAdminId = 0, $sendSubscriptionNotification = false, - $status = 0 + $status = 0, + $parentId = 0, + $daysBeforeFinishingForReinscription = null, + $daysBeforeFinishingToCreateNewRepetition = null, + $lastRepetition = false ) { $id = (int) $id; $status = (int) $status; @@ -1893,6 +1880,16 @@ public static function edit_session( ->setCoachAccessEndDate(null) ; + if ($parentId) { + $sessionEntity->setParentId($parentId); + } else { + $sessionEntity->setParentId(null); + } + + $sessionEntity->setDaysToReinscription($daysBeforeFinishingForReinscription); + $sessionEntity->setLastRepetition($lastRepetition); + $sessionEntity->setDaysToNewRepetition($daysBeforeFinishingToCreateNewRepetition); + $newGeneralCoaches = array_map( fn($coachId) => api_get_user_entity($coachId), $coachesId @@ -8214,6 +8211,51 @@ public static function setForm(FormValidator $form, Session $session = null, $fr $extra_field = new ExtraFieldModel('session'); $extra = $extra_field->addElements($form, $session ? $session->getId() : 0, ['image']); + + if ('true' === api_get_setting('session.enable_auto_reinscription')) { + $form->addElement( + 'text', + 'days_before_finishing_for_reinscription', + get_lang('DaysBeforeFinishingForReinscription'), + ['maxlength' => 5] + ); + } + + if ('true' === api_get_setting('session.enable_session_replication')) { + $form->addElement( + 'text', + 'days_before_finishing_to_create_new_repetition', + get_lang('DaysBeforeFinishingToCreateNewRepetition'), + ['maxlength' => 5] + ); + } + + if ('true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication')) { + $form->addElement( + 'checkbox', + 'last_repetition', + get_lang('LastRepetition') + ); + } + + /** @var HTML_QuickForm_select $element */ + $element = $form->createElement( + 'select', + 'parent_id', + get_lang('ParentSession'), + [], + ['class' => 'form-control'] + ); + + $element->addOption(get_lang('None'), 0, []); + $sessions = SessionManager::getListOfParentSessions(); + foreach ($sessions as $id => $title) { + $attributes = []; + $element->addOption($title, $id, $attributes); + } + + $form->addElement($element); + $form->addElement('html', ''); $js = $extra['jquery_ready_content']; @@ -10159,4 +10201,21 @@ public static function getAllUserIdsInSession(int $sessionId): array return $users; } + + /** + * Retrieves a list of parent sessions. + */ + public static function getListOfParentSessions(): array + { + $sessions = []; + $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION); + $sql = "SELECT id, title FROM $tbl_session WHERE parent_id IS NULL ORDER BY title"; + $result = Database::query($sql); + + while ($row = Database::fetch_array($result)) { + $sessions[$row['id']] = $row['title']; + } + + return $sessions; + } } diff --git a/public/main/lp/lp_add.php b/public/main/lp/lp_add.php index ad844eb0b7a..49694d65320 100644 --- a/public/main/lp/lp_add.php +++ b/public/main/lp/lp_add.php @@ -148,6 +148,21 @@ function activate_end_date() { SkillModel::addSkillsToForm($form, ITEM_TYPE_LEARNPATH, 0); +$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); +if ($showValidityField) { + $form->addElement( + 'number', + 'validity_in_days', + get_lang('Validity in days'), + [ + 'min' => 0, + 'max' => 365, + 'step' => 1, + 'placeholder' => get_lang('Enter the number of days'), + ] + ); +} + $form->addElement('html', ''); $defaults['activate_start_date_check'] = 1; @@ -208,6 +223,8 @@ function activate_end_date() { $lp->setSubscribeUsers(isset($_REQUEST['subscribe_users']) ? 1 : 0); $lp->setAccumulateScormTime(1 === (int) $_REQUEST['accumulate_scorm_time'] ? 1 : 0); + $validityInDays = $_REQUEST['validity_in_days'] ?? null; + $lp->setValidityInDays($validityInDays); $lpRepo->update($lp); $url = api_get_self().'?action=add_item&type=step&lp_id='.$lpId.'&'.api_get_cidreq(); diff --git a/public/main/lp/lp_edit.php b/public/main/lp/lp_edit.php index 1214199f583..e26bbadeed8 100644 --- a/public/main/lp/lp_edit.php +++ b/public/main/lp/lp_edit.php @@ -137,6 +137,21 @@ function activate_end_date() { ['jpg', 'jpeg', 'png', 'gif'] ); +$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); +if ($showValidityField) { + $form->addElement( + 'number', + 'validity_in_days', + get_lang('Validity in days'), + [ + 'min' => 0, + 'max' => 365, + 'step' => 1, + 'placeholder' => get_lang('Enter the number of days'), + ] + ); +} + // Search terms (only if search is activated). if ('true' === api_get_setting('search_enabled')) { $specific_fields = get_specific_field_list(); @@ -166,6 +181,7 @@ function activate_end_date() { $defaults['hide_toc_frame'] = $hideTableOfContents; $defaults['category_id'] = $learnPath->getCategoryId(); $defaults['accumulate_scorm_time'] = $learnPath->getAccumulateScormTime(); +$defaults['validity_in_days'] = $lp->getValidityInDays() ?? null; $expired_on = $learnPath->expired_on; $published_on = $learnPath->published_on; @@ -363,6 +379,7 @@ function activate_end_date() { ->setExpiredOn(api_get_utc_datetime($expired_on, true, true)) ->setCategory($category) ->setSubscribeUsers(isset($_REQUEST['subscribe_users']) ? 1 : 0) + ->setValidityInDays($_REQUEST['validity_in_days'] ?? null) ; $extraFieldValue = new ExtraFieldValue('lp'); diff --git a/public/main/session/session_add.php b/public/main/session/session_add.php index 2738129020f..30e6d434c5d 100644 --- a/public/main/session/session_add.php +++ b/public/main/session/session_add.php @@ -38,7 +38,7 @@ function search_coachs($needle) if (!empty($needle)) { $order_clause = api_sort_by_first_name() ? ' ORDER BY firstname, lastname, username' : ' ORDER BY lastname, firstname, username'; - // search users where username or firstname or lastname begins likes $needle + // search users where username or firstname or lastname begins like $needle $sql = 'SELECT username, lastname, firstname FROM '.$tbl_user.' user WHERE (username LIKE "'.$needle.'%" @@ -57,7 +57,7 @@ function search_coachs($needle) INNER JOIN '.$tbl_user_rel_access_url.' url_user ON (url_user.user_id=user.user_id) WHERE - access_url_id = '.$access_url_id.' AND + access_url_id = '.$access_url_id.' AND ( username LIKE "'.$needle.'%" OR firstname LIKE "'.$needle.'%" OR @@ -260,15 +260,12 @@ function (User $user) { $endDate = $params['access_end_date']; $displayStartDate = $params['display_start_date']; $displayEndDate = $params['display_end_date']; - $coachStartDate = $params['coach_access_start_date']; - if (empty($coachStartDate)) { - $coachStartDate = $displayStartDate; - } + $coachStartDate = $params['coach_access_start_date'] ?? $displayStartDate; $coachEndDate = $params['coach_access_end_date']; $coachUsername = $params['coach_username']; $id_session_category = (int) $params['session_category']; $id_visibility = $params['session_visibility']; - $duration = isset($params['duration']) ? $params['duration'] : null; + $duration = $params['duration'] ?? null; $description = $params['description']; $showDescription = isset($params['show_description']) ? 1 : 0; $sendSubscriptionNotification = isset($params['send_subscription_notification']); @@ -309,6 +306,12 @@ function (User $user) { } } } + $status = $params['status'] ?? 0; + + $parentId = $params['parent_id'] ?? null; + $daysBeforeFinishingForReinscription = $params['days_before_finishing_for_reinscription'] ?? null; + $lastRepetition = isset($params['last_repetition']) ? true : false; + $daysBeforeFinishingToCreateNewRepetition = $params['days_before_finishing_to_create_new_repetition'] ?? null; $return = SessionManager::create_session( $title, @@ -321,6 +324,10 @@ function (User $user) { $coachUsername, $id_session_category, $id_visibility, + $parentId, + $daysBeforeFinishingForReinscription, + $lastRepetition, + $daysBeforeFinishingToCreateNewRepetition, false, $duration, $description, diff --git a/public/main/session/session_edit.php b/public/main/session/session_edit.php index fe2fb84e506..57981c84139 100644 --- a/public/main/session/session_edit.php +++ b/public/main/session/session_edit.php @@ -71,6 +71,10 @@ function (User $user) { }, $session->getGeneralCoaches()->getValues() ), + 'days_before_finishing_for_reinscription' => $session->getDaysToReinscription() ?? '', + 'days_before_finishing_to_create_new_repetition' => $session->getDaysToNewRepetition() ?? '', + 'last_repetition' => $session->getLastRepetition(), + 'parent_id' => $session->getParentId() ?? 0, ]; $form->setDefaults($formDefaults); @@ -111,6 +115,11 @@ function (User $user) { $status = $params['status'] ?? 0; + $parentId = $params['parent_id'] ?? 0; + $daysBeforeFinishingForReinscription = $params['days_before_finishing_for_reinscription'] ?? null; + $daysBeforeFinishingToCreateNewRepetition = $params['days_before_finishing_to_create_new_repetition'] ?? null; + $lastRepetition = isset($params['last_repetition']); + $return = SessionManager::edit_session( $id, $name, @@ -129,7 +138,11 @@ function (User $user) { $extraFields, null, $sendSubscriptionNotification, - $status + $status, + $parentId, + $daysBeforeFinishingForReinscription, + $daysBeforeFinishingToCreateNewRepetition, + $lastRepetition ); if ($return) { diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index bb8e9d5db59..ccf6eddcd82 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -96,7 +96,7 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface // Create a new session with the same details as the original session $newSession = new Session(); $newSession - ->setTitle($session->getTitle() . ' (Repetition ' . $session->getId() . ')') + ->setTitle($session->getTitle() . ' (Repetition ' . $session->getId() . ' - ' . time() . ')') ->setAccessStartDate($newStartDate) ->setAccessEndDate($newEndDate) ->setDisplayStartDate($newStartDate) @@ -106,7 +106,7 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface ->setVisibility($session->getVisibility()) ->setDuration($session->getDuration()) ->setDescription($session->getDescription() ?? '') - ->setShowDescription($session->getShowDescription()) + ->setShowDescription($session->getShowDescription() ?? false) ->setCategory($session->getCategory()) ->setPromotion($session->getPromotion()) ->setLastRepetition(false); diff --git a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php index 5735e9eae0d..01e0da4fb43 100644 --- a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php +++ b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php @@ -3291,6 +3291,16 @@ public static function getNewConfigurationSettings(): array 'title' => 'Sort session templates by id in session creation form', 'comment' => '', ], + [ + 'name' => 'enable_auto_reinscription', + 'title' => 'Enable Automatic Reinscription', + 'comment' => 'Enable or disable automatic reinscription when course validity expires. The related cron job must also be activated.', + ], + [ + 'name' => 'enable_session_replication', + 'title' => 'Enable Session Replication', + 'comment' => 'Enable or disable automatic session replication. The related cron job must also be activated.', + ], [ 'name' => 'session_multiple_subscription_students_list_avoid_emptying', 'title' => 'Prevent emptying the subscribed users in session subscription', diff --git a/src/CoreBundle/Entity/Session.php b/src/CoreBundle/Entity/Session.php index 84bd14b2ea9..ae95c9ddf5e 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -449,7 +449,7 @@ public function setDuration(int $duration): self public function getShowDescription(): bool { - return $this->showDescription; + return $this->showDescription ?? false; } public function setShowDescription(bool $showDescription): self diff --git a/src/CoreBundle/Settings/SessionSettingsSchema.php b/src/CoreBundle/Settings/SessionSettingsSchema.php index d5f2da7dbcb..44072bc8ca7 100644 --- a/src/CoreBundle/Settings/SessionSettingsSchema.php +++ b/src/CoreBundle/Settings/SessionSettingsSchema.php @@ -79,6 +79,8 @@ public function buildSettings(AbstractSettingsBuilder $builder): void 'session_creation_user_course_extra_field_relation_to_prefill' => '', 'session_creation_form_set_extra_fields_mandatory' => '', 'session_model_list_field_ordered_by_id' => 'false', + 'enable_auto_reinscription' => 'false', + 'enable_session_replication' => 'false', ] ) ; @@ -217,6 +219,8 @@ public function buildForm(FormBuilderInterface $builder): void ] ) ->add('session_model_list_field_ordered_by_id', YesNoType::class) + ->add('enable_auto_reinscription', YesNoType::class) + ->add('enable_session_replication', YesNoType::class) ; $this->updateFormFieldsFromSettingsInfo($builder); From 2bf9adfb1eb48b9c196439caf4871d043f2c9976 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 26 Nov 2024 00:41:10 -0500 Subject: [PATCH 03/11] Internal: Add missing settings in migration - refs BT#22057 --- .../Schema/V200/Version20240928003000.php | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php index 7731ab6cbb6..d60f7cf269d 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php @@ -8,13 +8,12 @@ use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo; use Doctrine\DBAL\Schema\Schema; -use Doctrine\DBAL\Schema\Table; final class Version20240928003000 extends AbstractMigrationChamilo { public function getDescription(): string { - return 'Add new fields to session and c_lp tables for handling reinscription and session repetition logic'; + return 'Add new fields to session and c_lp tables for handling reinscription and session repetition logic, and insert new settings if not exist.'; } public function up(Schema $schema): void @@ -39,7 +38,7 @@ public function up(Schema $schema): void } } - // Add the field to the 'c_lp' (Learnpath) table + // Add fields to the 'c_lp' (Learnpath) table if ($schemaManager->tablesExist('c_lp')) { $clpTable = $schemaManager->listTableColumns('c_lp'); @@ -47,6 +46,23 @@ public function up(Schema $schema): void $this->addSql("ALTER TABLE c_lp ADD validity_in_days INT DEFAULT NULL"); } } + + // Insert new settings if not exist + $this->addSql(" + INSERT INTO settings (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) + SELECT 'enable_auto_reinscription', NULL, NULL, 'session', '0', 'Enable Auto Reinscription', 'Allow users to be automatically reinscribed in new sessions.', '', NULL, 1, 1, 1 + WHERE NOT EXISTS ( + SELECT 1 FROM settings WHERE variable = 'enable_auto_reinscription' + ) + "); + + $this->addSql(" + INSERT INTO settings (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) + SELECT 'enable_session_replication', NULL, NULL, 'session', '0', 'Enable Session Replication', 'Allow replication of session data across instances.', '', NULL, 1, 1, 1 + WHERE NOT EXISTS ( + SELECT 1 FROM settings WHERE variable = 'enable_session_replication' + ) + "); } public function down(Schema $schema): void @@ -79,5 +95,9 @@ public function down(Schema $schema): void $this->addSql("ALTER TABLE c_lp DROP COLUMN validity_in_days"); } } + + // Remove settings + $this->addSql("DELETE FROM settings WHERE variable = 'enable_auto_reinscription'"); + $this->addSql("DELETE FROM settings WHERE variable = 'enable_session_replication'"); } } From 66a227dd0c882128392bf709e68cf81a3c42f0d0 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 26 Nov 2024 22:50:58 -0500 Subject: [PATCH 04/11] Internal: Fix error type for validity_id_days field --- public/main/lp/lp_edit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/main/lp/lp_edit.php b/public/main/lp/lp_edit.php index e26bbadeed8..dfb79e88d7c 100644 --- a/public/main/lp/lp_edit.php +++ b/public/main/lp/lp_edit.php @@ -379,7 +379,7 @@ function activate_end_date() { ->setExpiredOn(api_get_utc_datetime($expired_on, true, true)) ->setCategory($category) ->setSubscribeUsers(isset($_REQUEST['subscribe_users']) ? 1 : 0) - ->setValidityInDays($_REQUEST['validity_in_days'] ?? null) + ->setValidityInDays(isset($_REQUEST['validity_in_days']) ? (int) $_REQUEST['validity_in_days'] : null) ; $extraFieldValue = new ExtraFieldValue('lp'); From 353c19ad6a8c70877d9b2e3c6d204656a41067d7 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Wed, 27 Nov 2024 17:43:36 -0500 Subject: [PATCH 05/11] Internal: Fix session duplication and reinscription logic improvements - refs BT#22057 --- public/main/inc/lib/sessionmanager.lib.php | 19 ++++++++++-- .../Command/ReinscriptionCheckCommand.php | 13 ++------- .../Command/SessionRepetitionCommand.php | 29 ++++++++++--------- .../Repository/SessionRepository.php | 13 ++++----- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index 0a73af77148..766a555bc1e 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -8305,6 +8305,12 @@ public static function setForm(FormValidator $form, Session $session = null, $fr get_lang('Days before finishing for reinscription'), ['maxlength' => 5] ); + $form->addRule( + 'days_before_finishing_for_reinscription', + get_lang('Days must be a positive number or empty'), + 'regex', + '/^\d*$/' + ); } if ('true' === api_get_setting('session.enable_session_replication')) { @@ -8314,6 +8320,12 @@ public static function setForm(FormValidator $form, Session $session = null, $fr get_lang('Days before finishing to create new repetition'), ['maxlength' => 5] ); + $form->addRule( + 'days_before_finishing_to_create_new_repetition', + get_lang('Days must be a positive number or empty'), + 'regex', + '/^\d*$/' + ); } if ('true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication')) { @@ -8335,9 +8347,12 @@ public static function setForm(FormValidator $form, Session $session = null, $fr $element->addOption(get_lang('None'), 0, []); $sessions = SessionManager::getListOfParentSessions(); + $currentSessionId = $session?->getId(); foreach ($sessions as $id => $title) { - $attributes = []; - $element->addOption($title, $id, $attributes); + if ($id !== $currentSessionId) { + $attributes = []; + $element->addOption($title, $id, $attributes); + } } $form->addElement($element); diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 81ca7f9d569..675339fd0e1 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -156,22 +156,13 @@ private function findExpiredCompletions($lp, $validityDays) */ private function enrollUserInSession($user, $session): void { - // First, check if the user is already enrolled in the session $existingSubscription = $this->findUserSubscriptionInSession($user, $session); - if ($existingSubscription) { - // Remove existing subscription before re-enrolling the user - $session->removeUserSubscription($existingSubscription); + if (!$existingSubscription) { + $session->addUserInSession(Session::STUDENT, $user); $this->entityManager->persist($session); $this->entityManager->flush(); } - - // Add the user into the session as a student - $session->addUserInSession(Session::STUDENT, $user); - - // Save the changes to the database - $this->entityManager->persist($session); - $this->entityManager->flush(); } private function findUserSubscriptionInSession($user, $session) diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index ccf6eddcd82..248def92eed 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -86,7 +86,8 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface $newEndDate = (clone $newStartDate)->add($duration); if ($debug) { - $output->writeln(sprintf('Duplicating session %d. New start date: %s, New end date: %s', + $output->writeln(sprintf( + 'Duplicating session %d. New start date: %s, New end date: %s', $session->getId(), $newStartDate->format('Y-m-d H:i:s'), $newEndDate->format('Y-m-d H:i:s') @@ -109,24 +110,24 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface ->setShowDescription($session->getShowDescription() ?? false) ->setCategory($session->getCategory()) ->setPromotion($session->getPromotion()) + ->setDaysToReinscription($session->getDaysToReinscription()) + ->setDaysToNewRepetition($session->getDaysToNewRepetition()) + ->setParentId($session->getId()) ->setLastRepetition(false); // Copy the AccessUrls from the original session - $accessUrls = $session->getUrls(); + foreach ($session->getUrls() as $accessUrl) { + $newSession->addAccessUrl($accessUrl->getUrl()); + } - if ($accessUrls->isEmpty()) { - // Handle the case where the session does not have any AccessUrl - if ($debug) { - $output->writeln('No AccessUrl found for session ' . $session->getId() . '. Assigning default AccessUrl.'); - } + // Copy the courses from the original session + foreach ($session->getCourses() as $course) { + $newSession->addCourse($course); + } - // Retrieve or create a default AccessUrl (you need to adjust this based on your system's needs) - $defaultAccessUrl = $this->getDefaultAccessUrl(); - $newSession->addAccessUrl($defaultAccessUrl); - } else { - foreach ($accessUrls as $accessUrl) { - $newSession->addAccessUrl($accessUrl->getUrl()); - } + // Copy the general coaches from the original session + foreach ($session->getGeneralCoaches() as $coach) { + $newSession->addGeneralCoach($coach); } // Save the new session diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index 5f0f1104ad1..d0227201088 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -473,22 +473,20 @@ public function getSubscribedSessionsOfUserInUrl( public function findValidChildSession(Session $session): ?Session { $childSessions = $this->findChildSessions($session); + $now = new \DateTime(); + foreach ($childSessions as $childSession) { - $now = new \DateTime(); $startDate = $childSession->getAccessStartDate(); $endDate = $childSession->getAccessEndDate(); $daysToReinscription = $childSession->getDaysToReinscription(); - // Skip if days to reinscription is not set - if ($daysToReinscription === null || $daysToReinscription === '') { + if (empty($daysToReinscription) || $daysToReinscription <= 0) { continue; } - // Adjust the end date by days to reinscription - $endDate = $endDate->modify('-' . $daysToReinscription . ' days'); + $adjustedEndDate = (clone $endDate)->modify('-' . $daysToReinscription . ' days'); - // Check if the current date falls within the session's validity period - if ($startDate <= $now && $endDate >= $now) { + if ($startDate <= $now && $adjustedEndDate >= $now) { return $childSession; } } @@ -561,6 +559,7 @@ public function findSessionsWithoutChildAndReadyForRepetition() ->andWhere('s.daysToNewRepetition IS NOT NULL') ->andWhere('s.lastRepetition = :false') ->andWhere(':currentDate BETWEEN DATE_SUB(s.accessEndDate, s.daysToNewRepetition, \'DAY\') AND s.accessEndDate') + ->andWhere('NOT EXISTS (SELECT 1 FROM Chamilo\CoreBundle\Entity\Session child WHERE child.parentId = s.id)') ->setParameter('false', false) ->setParameter('currentDate', $currentDate); From e9cca0b6ac2733af180ab78c9711533b26348521 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Thu, 28 Nov 2024 10:10:19 -0500 Subject: [PATCH 06/11] Internal: Improve session reinscription logic using direct CLpView validation - refs BT#22057 --- .../Command/ReinscriptionCheckCommand.php | 95 +++++++++---------- .../Command/SessionRepetitionCommand.php | 3 +- src/CourseBundle/Repository/CLpRepository.php | 15 ++- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 675339fd0e1..52f4b5f00dc 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -53,76 +53,67 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $debug = $input->getOption('debug'); - // 1. Find all lessons with "validity_in_days" > 0 - $learningPaths = $this->lpRepository->findWithValidity(); + $expiredViews = $this->lpRepository->findExpiredViews(0); - /* @var CLp $lp */ - foreach ($learningPaths as $lp) { - $validityDays = $lp->getValidityInDays(); - $sessionId = $this->lpRepository->getLpSessionId($lp->getIid()); - - if (!$sessionId) { - if ($debug) { - $output->writeln('Session ID not found for Learning Path ID: ' . $lp->getIid()); - } - continue; - } + if ($debug) { + $output->writeln(sprintf('Found %d expired views.', count($expiredViews))); + } - // 2. Get the session of the lesson - $session = $this->sessionRepository->find($sessionId); - if (!$session) { - if ($debug) { - $output->writeln('Session not found for ID: ' . $sessionId); - } - continue; + foreach ($expiredViews as $view) { + $user = $view->getUser(); + $session = $view->getSession(); + $lp = $view->getLp(); + + if ($debug) { + $output->writeln(sprintf( + 'User %d completed course %d associated with session %d, and its validity has expired.', + $user->getId(), + $lp->getIid(), + $session->getId() + )); } - // Process only if the session is not the last repetition + // Check if the session is marked as the last repetition if ($session->getLastRepetition()) { if ($debug) { - $output->writeln('Session ' . $session->getId() . ' is the last repetition. Skipping...'); + $output->writeln('The session is marked as the last repetition. Skipping...'); } continue; } - // 3. Find users who completed the lesson and whose validity has expired - $expiredUsers = $this->findExpiredCompletions($lp, $validityDays); + // Find a valid child session + $validChildSession = $this->sessionRepository->findValidChildSession($session); - if (count($expiredUsers) === 0) { + if ($validChildSession) { + $this->enrollUserInSession($user, $validChildSession); if ($debug) { - $output->writeln('No expired users found for Learning Path ID: ' . $lp->getIid()); + $output->writeln(sprintf( + 'User %d re-enrolled into the valid child session %d.', + $user->getId(), + $validChildSession->getId() + )); } continue; } - foreach ($expiredUsers as $user) { + // If no valid child session exists, check the parent session + $validParentSession = $this->sessionRepository->findValidParentSession($session); + + if ($validParentSession) { + $this->enrollUserInSession($user, $validParentSession); if ($debug) { - $output->writeln('User ' . $user->getUser()->getId() . ' has expired completion for LP ' . $lp->getIid()); + $output->writeln(sprintf( + 'User %d re-enrolled into the valid parent session %d.', + $user->getId(), + $validParentSession->getId() + )); } - - // 4. Find the last valid child session - $validChildSession = $this->sessionRepository->findValidChildSession($session); - - if ($validChildSession) { - // Reinscribe user in the valid child session - $this->enrollUserInSession($user->getUser(), $validChildSession); - if ($debug) { - $output->writeln('Reinscribed user ' . $user->getUser()->getId() . ' into child session ' . $validChildSession->getId()); - } - } else { - // 5. If no valid child session, find the valid parent session - $validParentSession = $this->sessionRepository->findValidParentSession($session); - if ($validParentSession) { - // Reinscribe user in the valid parent session - $this->enrollUserInSession($user->getUser(), $validParentSession); - if ($debug) { - $output->writeln('Reinscribed user ' . $user->getUser()->getId() . ' into parent session ' . $validParentSession->getId()); - } - } else { - if ($debug) { - $output->writeln('No valid parent or child session found for user ' . $user->getUser()->getId()); - } - } + } else { + if ($debug) { + $output->writeln(sprintf( + 'No valid child or parent session found for user %d.', + $user->getId() + )); } } } diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index 248def92eed..0280698cb7a 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -121,7 +121,8 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface } // Copy the courses from the original session - foreach ($session->getCourses() as $course) { + foreach ($session->getCourses() as $sessionRelCourse) { + $course = $sessionRelCourse->getCourse(); $newSession->addCourse($course); } diff --git a/src/CourseBundle/Repository/CLpRepository.php b/src/CourseBundle/Repository/CLpRepository.php index 83449e1ee9b..f7bfc9a399d 100644 --- a/src/CourseBundle/Repository/CLpRepository.php +++ b/src/CourseBundle/Repository/CLpRepository.php @@ -138,10 +138,21 @@ public function getLpSessionId(int $lpId): ?int return null; } - public function findWithValidity(): array + public function findExpiredViews(int $validityDays): array { - return $this->createQueryBuilder('lp') + $now = new \DateTime(); + $expirationDate = (clone $now)->modify('-' . $validityDays . ' days'); + + return $this->getEntityManager() + ->createQueryBuilder() + ->select('v') + ->from('Chamilo\CourseBundle\Entity\CLpView', 'v') + ->join('v.lp', 'lp') ->where('lp.validityInDays > 0') + ->andWhere('v.progress = 100') + ->andWhere('v.session IS NOT NULL') + ->andWhere('v.lastItem < :expirationDate') + ->setParameter('expirationDate', $expirationDate->getTimestamp()) ->getQuery() ->getResult(); } From ba0914061da41ad812f9aa45ff1017076a85844a Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Fri, 29 Nov 2024 17:49:53 -0500 Subject: [PATCH 07/11] Internal: Fix session duplication and reinscription logic - refs BT#22057 --- .../Command/ReinscriptionCheckCommand.php | 67 ++++++++++--------- .../Command/SessionRepetitionCommand.php | 6 +- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 52f4b5f00dc..d5fab996521 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -73,45 +73,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); } - // Check if the session is marked as the last repetition - if ($session->getLastRepetition()) { - if ($debug) { - $output->writeln('The session is marked as the last repetition. Skipping...'); - } - continue; - } - - // Find a valid child session - $validChildSession = $this->sessionRepository->findValidChildSession($session); + $validSession = $this->findValidSessionInHierarchy($session); - if ($validChildSession) { - $this->enrollUserInSession($user, $validChildSession); + if ($validSession) { + $this->enrollUserInSession($user, $validSession); if ($debug) { $output->writeln(sprintf( - 'User %d re-enrolled into the valid child session %d.', + 'User %d re-enrolled into session %d.', $user->getId(), - $validChildSession->getId() - )); - } - continue; - } - - // If no valid child session exists, check the parent session - $validParentSession = $this->sessionRepository->findValidParentSession($session); - - if ($validParentSession) { - $this->enrollUserInSession($user, $validParentSession); - if ($debug) { - $output->writeln(sprintf( - 'User %d re-enrolled into the valid parent session %d.', - $user->getId(), - $validParentSession->getId() + $validSession->getId() )); } } else { if ($debug) { $output->writeln(sprintf( - 'No valid child or parent session found for user %d.', + 'No valid session found for user %d.', $user->getId() )); } @@ -164,4 +140,35 @@ private function findUserSubscriptionInSession($user, $session) 'session' => $session, ]); } + + private function findValidSessionInHierarchy(Session $session): ?Session + { + $childSessions = $this->sessionRepository->findChildSessions($session); + + /* @var Session $child */ + foreach ($childSessions as $child) { + $validUntil = (clone $child->getAccessEndDate())->modify("-{$child->getDaysToReinscription()} days"); + if (new \DateTime() <= $validUntil) { + return $child; + } + + $validChild = $this->findValidSessionInHierarchy($child); + if ($validChild) { + return $validChild; + } + } + + /* @var Session $parentSession */ + $parentSession = $this->sessionRepository->findParentSession($session); + + if ($parentSession) { + $validUntil = (clone $parentSession->getAccessEndDate())->modify("-{$parentSession->getDaysToReinscription()} days"); + if (new \DateTime() <= $validUntil) { + return $parentSession; + } + } + + return null; + } + } diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index 0280698cb7a..27b1d015fa9 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -81,9 +81,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function duplicateSession(Session $session, bool $debug, OutputInterface $output): Session { // Calculate new session dates based on the duration of the original session - $duration = $session->getAccessEndDate()->diff($session->getAccessStartDate()); + $duration = $session->getAccessEndDate()->diff($session->getAccessStartDate())->days; $newStartDate = (clone $session->getAccessEndDate())->modify('+1 day'); - $newEndDate = (clone $newStartDate)->add($duration); + $newEndDate = (clone $newStartDate)->modify("+{$duration} days"); if ($debug) { $output->writeln(sprintf( @@ -105,7 +105,7 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface ->setCoachAccessStartDate($newStartDate) ->setCoachAccessEndDate($newEndDate) ->setVisibility($session->getVisibility()) - ->setDuration($session->getDuration()) + ->setDuration($duration) ->setDescription($session->getDescription() ?? '') ->setShowDescription($session->getShowDescription() ?? false) ->setCategory($session->getCategory()) From 7b665990e710068f93b4856a8cc14f12e057b733 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Wed, 4 Dec 2024 13:14:11 -0500 Subject: [PATCH 08/11] Internal: Implement automatic session repetition and coach notifications - refs BT#22057 --- .../Command/ReinscriptionCheckCommand.php | 20 +++++++++++++++++++ .../Command/SessionRepetitionCommand.php | 6 ++++-- .../Repository/SessionRepository.php | 7 ++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index d5fab996521..3a5538d3752 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -62,6 +62,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($expiredViews as $view) { $user = $view->getUser(); $session = $view->getSession(); + + if ($this->isUserAlreadyEnrolledInChildSession($user, $session)) { + if ($debug) { + $output->writeln(sprintf('User %d is already enrolled in a valid child session.', $user->getId())); + } + continue; + } + $lp = $view->getLp(); if ($debug) { @@ -171,4 +179,16 @@ private function findValidSessionInHierarchy(Session $session): ?Session return null; } + private function isUserAlreadyEnrolledInChildSession($user, $parentSession): bool + { + $childSessions = $this->sessionRepository->findChildSessions($parentSession); + + foreach ($childSessions as $childSession) { + if ($this->findUserSubscriptionInSession($user, $childSession)) { + return true; + } + } + + return false; + } } diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index 27b1d015fa9..aee12a1f0d6 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -105,7 +105,7 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface ->setCoachAccessStartDate($newStartDate) ->setCoachAccessEndDate($newEndDate) ->setVisibility($session->getVisibility()) - ->setDuration($duration) + ->setDuration(0) ->setDescription($session->getDescription() ?? '') ->setShowDescription($session->getShowDescription() ?? false) ->setCategory($session->getCategory()) @@ -123,7 +123,9 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface // Copy the courses from the original session foreach ($session->getCourses() as $sessionRelCourse) { $course = $sessionRelCourse->getCourse(); - $newSession->addCourse($course); + if ($course) { + $newSession->addCourse($course); + } } // Copy the general coaches from the original session diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index d0227201088..da0ff1de3f9 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -559,7 +559,12 @@ public function findSessionsWithoutChildAndReadyForRepetition() ->andWhere('s.daysToNewRepetition IS NOT NULL') ->andWhere('s.lastRepetition = :false') ->andWhere(':currentDate BETWEEN DATE_SUB(s.accessEndDate, s.daysToNewRepetition, \'DAY\') AND s.accessEndDate') - ->andWhere('NOT EXISTS (SELECT 1 FROM Chamilo\CoreBundle\Entity\Session child WHERE child.parentId = s.id)') + ->andWhere('NOT EXISTS ( + SELECT 1 + FROM Chamilo\CoreBundle\Entity\Session child + WHERE child.parentId = s.id + AND child.accessEndDate >= :currentDate + )') ->setParameter('false', false) ->setParameter('currentDate', $currentDate); From 3b34dc1fbe87b9ec007a924605b84a08cd236a25 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Wed, 18 Dec 2024 18:35:02 -0500 Subject: [PATCH 09/11] Internal: Reinscription logic updated to use sessions' validity and gradebook certificates - refs BT#22057 --- public/main/inc/lib/sessionmanager.lib.php | 30 +- public/main/lp/lp_add.php | 17 -- public/main/lp/lp_edit.php | 17 -- public/main/session/session_add.php | 13 +- public/main/session/session_edit.php | 9 +- public/phpinfo.php | 1 - .../Command/ReinscriptionCheckCommand.php | 261 ++++++++++++------ src/CoreBundle/Entity/Session.php | 15 + src/CoreBundle/Entity/SessionRelUser.php | 15 + .../Schema/V200/Version20240928003000.php | 48 +++- src/CourseBundle/Entity/CLp.php | 14 - src/CourseBundle/Repository/CLpRepository.php | 19 -- 12 files changed, 294 insertions(+), 165 deletions(-) delete mode 100755 public/phpinfo.php diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index cfa56348df7..68f23e8a0bb 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -163,7 +163,8 @@ public static function create_session( $parentId = null, $daysBeforeFinishingForReinscription = null, $lastRepetition = false, - $daysBeforeFinishingToCreateNewRepetition = null + $daysBeforeFinishingToCreateNewRepetition = null, + $validityInDays = null ) { global $_configuration; @@ -234,7 +235,8 @@ public static function create_session( ->setParentId($parentId) ->setDaysToReinscription((int) $daysBeforeFinishingForReinscription) ->setLastRepetition($lastRepetition) - ->setDaysToNewRepetition((int) $daysBeforeFinishingToCreateNewRepetition); + ->setDaysToNewRepetition((int) $daysBeforeFinishingToCreateNewRepetition) + ->setValidityInDays((int) $validityInDays); foreach ($coachesId as $coachId) { $session->addGeneralCoach(api_get_user_entity($coachId)); @@ -1806,7 +1808,8 @@ public static function edit_session( $parentId = 0, $daysBeforeFinishingForReinscription = null, $daysBeforeFinishingToCreateNewRepetition = null, - $lastRepetition = false + $lastRepetition = false, + $validityInDays = null ) { $id = (int) $id; $status = (int) $status; @@ -1880,6 +1883,7 @@ public static function edit_session( ->setDaysToReinscription((int) $daysBeforeFinishingForReinscription) ->setLastRepetition($lastRepetition) ->setDaysToNewRepetition((int) $daysBeforeFinishingToCreateNewRepetition) + ->setValidityInDays((int) $validityInDays) ->setAccessStartDate(null) ->setAccessStartDate(null) ->setDisplayStartDate(null) @@ -8334,6 +8338,26 @@ public static function setForm(FormValidator $form, Session $session = null, $fr 'last_repetition', get_lang('Last repetition') ); + + $form->addElement( + 'number', + 'validity_in_days', + get_lang('Validity in days'), + [ + 'min' => 0, + 'max' => 365, + 'step' => 1, + 'placeholder' => get_lang('Enter the number of days'), + ] + ); + + $form->addRule( + 'validity_in_days', + get_lang('The field must be a positive number'), + 'numeric', + null, + 'client' + ); } /** @var HTML_QuickForm_select $element */ diff --git a/public/main/lp/lp_add.php b/public/main/lp/lp_add.php index 49694d65320..ad844eb0b7a 100644 --- a/public/main/lp/lp_add.php +++ b/public/main/lp/lp_add.php @@ -148,21 +148,6 @@ function activate_end_date() { SkillModel::addSkillsToForm($form, ITEM_TYPE_LEARNPATH, 0); -$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); -if ($showValidityField) { - $form->addElement( - 'number', - 'validity_in_days', - get_lang('Validity in days'), - [ - 'min' => 0, - 'max' => 365, - 'step' => 1, - 'placeholder' => get_lang('Enter the number of days'), - ] - ); -} - $form->addElement('html', ''); $defaults['activate_start_date_check'] = 1; @@ -223,8 +208,6 @@ function activate_end_date() { $lp->setSubscribeUsers(isset($_REQUEST['subscribe_users']) ? 1 : 0); $lp->setAccumulateScormTime(1 === (int) $_REQUEST['accumulate_scorm_time'] ? 1 : 0); - $validityInDays = $_REQUEST['validity_in_days'] ?? null; - $lp->setValidityInDays($validityInDays); $lpRepo->update($lp); $url = api_get_self().'?action=add_item&type=step&lp_id='.$lpId.'&'.api_get_cidreq(); diff --git a/public/main/lp/lp_edit.php b/public/main/lp/lp_edit.php index b5cbfd1104a..4c60bb49bc3 100644 --- a/public/main/lp/lp_edit.php +++ b/public/main/lp/lp_edit.php @@ -137,21 +137,6 @@ function activate_end_date() { ['jpg', 'jpeg', 'png', 'gif'] ); -$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); -if ($showValidityField) { - $form->addElement( - 'number', - 'validity_in_days', - get_lang('Validity in days'), - [ - 'min' => 0, - 'max' => 365, - 'step' => 1, - 'placeholder' => get_lang('Enter the number of days'), - ] - ); -} - // Search terms (only if search is activated). if ('true' === api_get_setting('search_enabled')) { $specific_fields = get_specific_field_list(); @@ -181,7 +166,6 @@ function activate_end_date() { $defaults['hide_toc_frame'] = $hideTableOfContents; $defaults['category_id'] = $learnPath->getCategoryId(); $defaults['accumulate_scorm_time'] = $learnPath->getAccumulateScormTime(); -$defaults['validity_in_days'] = $lp->getValidityInDays() ?? null; $expired_on = $learnPath->expired_on; $published_on = $learnPath->published_on; @@ -379,7 +363,6 @@ function activate_end_date() { ->setExpiredOn(api_get_utc_datetime($expired_on, true, true)) ->setCategory($category) ->setSubscribeUsers(isset($_REQUEST['subscribe_users']) ? 1 : 0) - ->setValidityInDays(isset($_REQUEST['validity_in_days']) ? (int) $_REQUEST['validity_in_days'] : null) ; $extraFieldValue = new ExtraFieldValue('lp'); diff --git a/public/main/session/session_add.php b/public/main/session/session_add.php index 36ad63d7ab2..309b94170c6 100644 --- a/public/main/session/session_add.php +++ b/public/main/session/session_add.php @@ -219,6 +219,7 @@ function repopulateSelect2Values(selectId) { "; $form->addButtonNext(get_lang('Next step')); +$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); $formDefaults = []; if (!$formSent) { @@ -250,9 +251,17 @@ function (User $user) { 'last_repetition' => $session->getLastRepetition(), 'parent_id' => $session->getParentId() ?? 0, ]; + + if ($showValidityField) { + $formDefaults['validity_in_days'] = $session->getValidityInDays(); + } + } else { $formDefaults['access_start_date'] = $formDefaults['display_start_date'] = api_get_local_time(); $formDefaults['coach_username'] = [api_get_user_id()]; + if ($showValidityField) { + $formDefaults['validity_in_days'] = null; + } } } @@ -318,6 +327,7 @@ function (User $user) { $daysBeforeFinishingForReinscription = $params['days_before_finishing_for_reinscription'] ?? null; $lastRepetition = isset($params['last_repetition']) ? true : false; $daysBeforeFinishingToCreateNewRepetition = $params['days_before_finishing_to_create_new_repetition'] ?? null; + $validityInDays = $params['validity_in_days'] ?? null; $return = SessionManager::create_session( $title, @@ -343,7 +353,8 @@ function (User $user) { $parentId, $daysBeforeFinishingForReinscription, $lastRepetition, - $daysBeforeFinishingToCreateNewRepetition + $daysBeforeFinishingToCreateNewRepetition, + $validityInDays ); if ($return == strval(intval($return))) { diff --git a/public/main/session/session_edit.php b/public/main/session/session_edit.php index 77fa8d2ba53..ff791c6ff31 100644 --- a/public/main/session/session_edit.php +++ b/public/main/session/session_edit.php @@ -49,6 +49,7 @@ '; $form->addButtonUpdate(get_lang('Edit this session')); +$showValidityField = 'true' === api_get_setting('session.enable_auto_reinscription') || 'true' === api_get_setting('session.enable_session_replication'); $formDefaults = [ 'id' => $session->getId(), @@ -78,6 +79,10 @@ function (User $user) { 'parent_id' => $session->getParentId() ?? 0, ]; +if ($showValidityField) { + $formDefaults['validity_in_days'] = $session->getValidityInDays(); +} + $form->setDefaults($formDefaults); if ($form->validate()) { @@ -121,6 +126,7 @@ function (User $user) { $daysBeforeFinishingForReinscription = $params['days_before_finishing_for_reinscription'] ?? null; $daysBeforeFinishingToCreateNewRepetition = $params['days_before_finishing_to_create_new_repetition'] ?? null; $lastRepetition = isset($params['last_repetition']); + $validityInDays = $params['validity_in_days'] ?? null; $return = SessionManager::edit_session( $id, @@ -145,7 +151,8 @@ function (User $user) { $parentId, $daysBeforeFinishingForReinscription, $daysBeforeFinishingToCreateNewRepetition, - $lastRepetition + $lastRepetition, + $validityInDays ); if ($return) { diff --git a/public/phpinfo.php b/public/phpinfo.php deleted file mode 100755 index 147cebcdd47..00000000000 --- a/public/phpinfo.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 3a5538d3752..7136a0ccbac 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -8,10 +8,10 @@ use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\SessionRelUser; -use Chamilo\CourseBundle\Entity\CLp; -use Chamilo\CourseBundle\Repository\CLpRepository; +use Chamilo\CoreBundle\Entity\SessionRelCourse; +use Chamilo\CoreBundle\Entity\GradebookCertificate; use Chamilo\CoreBundle\Repository\SessionRepository; -use Chamilo\CourseBundle\Entity\CLpView; +use Chamilo\CoreBundle\Repository\GradebookCertificateRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -22,25 +22,25 @@ class ReinscriptionCheckCommand extends Command { protected static $defaultName = 'app:reinscription-check'; - private CLpRepository $lpRepository; private SessionRepository $sessionRepository; + private GradebookCertificateRepository $certificateRepository; private EntityManagerInterface $entityManager; public function __construct( - CLpRepository $lpRepository, SessionRepository $sessionRepository, + GradebookCertificateRepository $certificateRepository, EntityManagerInterface $entityManager ) { parent::__construct(); - $this->lpRepository = $lpRepository; $this->sessionRepository = $sessionRepository; + $this->certificateRepository = $certificateRepository; $this->entityManager = $entityManager; } protected function configure(): void { $this - ->setDescription('Checks for users whose course completions have expired and reinscribe them into new sessions if needed.') + ->setDescription('Checks for users who have validated all gradebooks and reinscribe them into new sessions if needed.') ->addOption( 'debug', null, @@ -53,93 +53,201 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $debug = $input->getOption('debug'); - $expiredViews = $this->lpRepository->findExpiredViews(0); + $sessions = $this->sessionRepository->findAll(); - if ($debug) { - $output->writeln(sprintf('Found %d expired views.', count($expiredViews))); - } + foreach ($sessions as $session) { + if ($session->getValidityInDays() === null) { + continue; + } - foreach ($expiredViews as $view) { - $user = $view->getUser(); - $session = $view->getSession(); + $users = $this->getUsersForSession($session); - if ($this->isUserAlreadyEnrolledInChildSession($user, $session)) { + foreach ($users as $user) { if ($debug) { - $output->writeln(sprintf('User %d is already enrolled in a valid child session.', $user->getId())); + $output->writeln(sprintf('Processing user %d in session %d.', $user->getId(), $session->getId())); + } + + if ($this->isUserReinscribed($user, $session)) { + continue; + } + + if ($this->isUserAlreadyEnrolledInChildSession($user, $session)) { + if ($debug) { + $output->writeln(sprintf('User %d is already enrolled in a valid child session.', $user->getId())); + } + continue; + } + + $certificates = $this->getUserCertificatesForSession($user, $session); + + if ($this->hasUserValidatedAllGradebooks($session, $certificates)) { + $latestValidationDate = $this->getLatestCertificateDate($certificates); + + if ($latestValidationDate !== null) { + $reinscriptionDate = (clone $latestValidationDate)->modify("+{$session->getValidityInDays()} days"); + + if ($debug) { + $output->writeln(sprintf( + 'User %d - Latest certificate date: %s, Reinscription date: %s', + $user->getId(), + $latestValidationDate->format('Y-m-d'), + $reinscriptionDate->format('Y-m-d') + )); + } + + if (new \DateTime() >= $reinscriptionDate) { + $validSession = $this->findValidSessionInHierarchy($session); + + if ($validSession) { + $this->enrollUserInSession($user, $validSession, $session); + if ($debug) { + $output->writeln(sprintf( + 'User %d re-enrolled into session %d.', + $user->getId(), + $validSession->getId() + )); + } + } + } + } else { + if ($debug) { + $output->writeln(sprintf( + 'User %d has no valid certificates for session %d.', + $user->getId(), + $session->getId() + )); + } + } } - continue; } + } - $lp = $view->getLp(); + return Command::SUCCESS; + } + + /** + * Retrieves all users associated with the session. + */ + private function getUsersForSession(Session $session): array + { + $usersToNotify = []; + $sessionCourses = $this->entityManager->getRepository(SessionRelCourse::class)->findBy(['session' => $session]); + + foreach ($sessionCourses as $courseRel) { + $course = $courseRel->getCourse(); - if ($debug) { - $output->writeln(sprintf( - 'User %d completed course %d associated with session %d, and its validity has expired.', - $user->getId(), - $lp->getIid(), - $session->getId() - )); + $studentSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT); + foreach ($studentSubscriptions as $studentSubscription) { + $usersToNotify[$studentSubscription->getUser()->getId()] = $studentSubscription->getUser(); } - $validSession = $this->findValidSessionInHierarchy($session); + $coachSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::COURSE_COACH); + foreach ($coachSubscriptions as $coachSubscription) { + $usersToNotify[$coachSubscription->getUser()->getId()] = $coachSubscription->getUser(); + } + } - if ($validSession) { - $this->enrollUserInSession($user, $validSession); - if ($debug) { - $output->writeln(sprintf( - 'User %d re-enrolled into session %d.', - $user->getId(), - $validSession->getId() - )); - } - } else { - if ($debug) { - $output->writeln(sprintf( - 'No valid session found for user %d.', - $user->getId() - )); - } + $generalCoaches = $session->getGeneralCoaches(); + foreach ($generalCoaches as $generalCoach) { + $usersToNotify[$generalCoach->getId()] = $generalCoach; + } + + return array_values($usersToNotify); + } + + /** + * Checks if the user is already enrolled in a valid child session. + */ + private function isUserAlreadyEnrolledInChildSession($user, $parentSession): bool + { + $childSessions = $this->sessionRepository->findChildSessions($parentSession); + + foreach ($childSessions as $childSession) { + if ($this->findUserSubscriptionInSession($user, $childSession)) { + return true; } } - return Command::SUCCESS; + return false; } /** - * Find users with expired completion based on "validity_in_days". + * Gets the user's certificates for the courses in the session. */ - private function findExpiredCompletions($lp, $validityDays) + private function getUserCertificatesForSession($user, Session $session): array { - $now = new \DateTime(); - $expirationDate = (clone $now)->modify('-' . $validityDays . ' days'); - - // Find users with 100% completion and whose last access date (start_time) is older than 'validity_in_days' - return $this->entityManager->getRepository(CLpView::class) - ->createQueryBuilder('v') - ->innerJoin('Chamilo\CourseBundle\Entity\CLpItemView', 'iv', 'WITH', 'iv.view = v') - ->where('v.lp = :lp') - ->andWhere('v.progress = 100') - ->andWhere('iv.startTime < :expirationDate') - ->setParameter('lp', $lp) - ->setParameter('expirationDate', $expirationDate->getTimestamp()) + $courses = $this->entityManager->getRepository(SessionRelCourse::class) + ->findBy(['session' => $session]); + + $courseIds = array_map(fn($rel) => $rel->getCourse()->getId(), $courses); + + return $this->certificateRepository->createQueryBuilder('gc') + ->join('gc.category', 'cat') + ->where('gc.user = :user') + ->andWhere('cat.course IN (:courses)') + ->setParameter('user', $user) + ->setParameter('courses', $courseIds) ->getQuery() ->getResult(); } /** - * Enrolls a user into a session. + * Checks if the user has validated all gradebooks in the session. + */ + private function hasUserValidatedAllGradebooks(Session $session, array $certificates): bool + { + $courses = $this->entityManager->getRepository(SessionRelCourse::class) + ->findBy(['session' => $session]); + + return count($certificates) === count($courses); + } + + /** + * Returns the latest certificate creation date. */ - private function enrollUserInSession($user, $session): void + private function getLatestCertificateDate(array $certificates): ?\DateTime { - $existingSubscription = $this->findUserSubscriptionInSession($user, $session); + $dates = array_map(fn($cert) => $cert->getCreatedAt(), $certificates); + + if (empty($dates)) { + return null; + } + + return max($dates); + } + + /** + * Enrolls the user in a new session and updates the previous session subscription. + */ + private function enrollUserInSession($user, $newSession, $oldSession): void + { + $existingSubscription = $this->findUserSubscriptionInSession($user, $newSession); if (!$existingSubscription) { - $session->addUserInSession(Session::STUDENT, $user); - $this->entityManager->persist($session); + $newSession->addUserInSession(Session::STUDENT, $user); + + $subscription = $this->findUserSubscriptionInSession($user, $oldSession); + if ($subscription) { + $subscription->setNewSubscriptionSessionId($newSession->getId()); + } + + $this->entityManager->persist($newSession); $this->entityManager->flush(); } } + /** + * Determines if the user has already been reinscribed. + */ + private function isUserReinscribed($user, Session $session): bool + { + $subscription = $this->findUserSubscriptionInSession($user, $session); + return $subscription && $subscription->getNewSubscriptionSessionId() !== null; + } + + /** + * Finds the user's subscription in the specified session. + */ private function findUserSubscriptionInSession($user, $session) { return $this->entityManager->getRepository(SessionRelUser::class) @@ -149,6 +257,9 @@ private function findUserSubscriptionInSession($user, $session) ]); } + /** + * Finds a valid session within the session hierarchy. + */ private function findValidSessionInHierarchy(Session $session): ?Session { $childSessions = $this->sessionRepository->findChildSessions($session); @@ -159,36 +270,14 @@ private function findValidSessionInHierarchy(Session $session): ?Session if (new \DateTime() <= $validUntil) { return $child; } - - $validChild = $this->findValidSessionInHierarchy($child); - if ($validChild) { - return $validChild; - } } - /* @var Session $parentSession */ $parentSession = $this->sessionRepository->findParentSession($session); - if ($parentSession) { - $validUntil = (clone $parentSession->getAccessEndDate())->modify("-{$parentSession->getDaysToReinscription()} days"); - if (new \DateTime() <= $validUntil) { - return $parentSession; - } + if ($parentSession && new \DateTime() <= $parentSession->getAccessEndDate()) { + return $parentSession; } return null; } - - private function isUserAlreadyEnrolledInChildSession($user, $parentSession): bool - { - $childSessions = $this->sessionRepository->findChildSessions($parentSession); - - foreach ($childSessions as $childSession) { - if ($this->findUserSubscriptionInSession($user, $childSession)) { - return true; - } - } - - return false; - } } diff --git a/src/CoreBundle/Entity/Session.php b/src/CoreBundle/Entity/Session.php index 9a55e0bbab9..4185ef89371 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -395,6 +395,10 @@ class Session implements ResourceWithAccessUrlInterface, Stringable #[ORM\Column(name: 'notify_boss', type: 'boolean', options: ['default' => false])] protected bool $notifyBoss = false; + #[Groups(['session:basic', 'session:read', 'session:write'])] + #[ORM\Column(name: 'validity_in_days', type: 'integer', nullable: true)] + protected ?int $validityInDays = null; + public function __construct() { $this->skills = new ArrayCollection(); @@ -1545,4 +1549,15 @@ public function setNotifyBoss(bool $notifyBoss): self return $this; } + + public function getValidityInDays(): ?int + { + return $this->validityInDays; + } + + public function setValidityInDays(?int $validityInDays): self + { + $this->validityInDays = $validityInDays; + return $this; + } } diff --git a/src/CoreBundle/Entity/SessionRelUser.php b/src/CoreBundle/Entity/SessionRelUser.php index 25b474e5c34..5b4d104e726 100644 --- a/src/CoreBundle/Entity/SessionRelUser.php +++ b/src/CoreBundle/Entity/SessionRelUser.php @@ -108,6 +108,9 @@ class SessionRelUser #[ORM\Column(name: 'collapsed', type: 'boolean', nullable: true, options: ['default' => null])] protected ?bool $collapsed = null; + #[ORM\Column(name: 'new_subscription_session_id', type: 'integer', nullable: true)] + protected ?int $newSubscriptionSessionId = null; + /** * @throws Exception */ @@ -226,4 +229,16 @@ public function setDuration(int $duration): self return $this; } + + public function getNewSubscriptionSessionId(): ?int + { + return $this->newSubscriptionSessionId; + } + + public function setNewSubscriptionSessionId(?int $newSubscriptionSessionId): self + { + $this->newSubscriptionSessionId = $newSubscriptionSessionId; + + return $this; + } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php index d60f7cf269d..4faec2e633d 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php @@ -20,6 +20,15 @@ public function up(Schema $schema): void { $schemaManager = $this->connection->createSchemaManager(); + // Add 'new_subscription_session_id' to the 'session_rel_user' table + if ($schemaManager->tablesExist('session_rel_user')) { + $sessionRelUserTable = $schemaManager->listTableColumns('session_rel_user'); + + if (!isset($sessionRelUserTable['new_subscription_session_id'])) { + $this->addSql("ALTER TABLE session_rel_user ADD new_subscription_session_id INT DEFAULT NULL"); + } + } + // Add fields to the 'session' table if ($schemaManager->tablesExist('session')) { $sessionTable = $schemaManager->listTableColumns('session'); @@ -38,12 +47,21 @@ public function up(Schema $schema): void } } - // Add fields to the 'c_lp' (Learnpath) table + // Add 'validity_in_days' to the 'session' table + if ($schemaManager->tablesExist('session')) { + $sessionTable = $schemaManager->listTableColumns('session'); + + if (!isset($sessionTable['validity_in_days'])) { + $this->addSql("ALTER TABLE session ADD validity_in_days INT DEFAULT NULL"); + } + } + + // Remove 'validity_in_days' from the 'c_lp' table if ($schemaManager->tablesExist('c_lp')) { $clpTable = $schemaManager->listTableColumns('c_lp'); - if (!isset($clpTable['validity_in_days'])) { - $this->addSql("ALTER TABLE c_lp ADD validity_in_days INT DEFAULT NULL"); + if (isset($clpTable['validity_in_days'])) { + $this->addSql("ALTER TABLE c_lp DROP COLUMN validity_in_days"); } } @@ -69,6 +87,15 @@ public function down(Schema $schema): void { $schemaManager = $this->connection->createSchemaManager(); + // Revert 'new_subscription_session_id' in the 'session_rel_user' table + if ($schemaManager->tablesExist('session_rel_user')) { + $sessionRelUserTable = $schemaManager->listTableColumns('session_rel_user'); + + if (isset($sessionRelUserTable['new_subscription_session_id'])) { + $this->addSql("ALTER TABLE session_rel_user DROP COLUMN new_subscription_session_id"); + } + } + // Revert changes in the 'session' table if ($schemaManager->tablesExist('session')) { $sessionTable = $schemaManager->listTableColumns('session'); @@ -87,12 +114,21 @@ public function down(Schema $schema): void } } - // Revert changes in the 'c_lp' table + // Revert 'validity_in_days' in the 'session' table + if ($schemaManager->tablesExist('session')) { + $sessionTable = $schemaManager->listTableColumns('session'); + + if (isset($sessionTable['validity_in_days'])) { + $this->addSql("ALTER TABLE session DROP COLUMN validity_in_days"); + } + } + + // Re-add 'validity_in_days' to the 'c_lp' table if ($schemaManager->tablesExist('c_lp')) { $clpTable = $schemaManager->listTableColumns('c_lp'); - if (isset($clpTable['validity_in_days'])) { - $this->addSql("ALTER TABLE c_lp DROP COLUMN validity_in_days"); + if (!isset($clpTable['validity_in_days'])) { + $this->addSql("ALTER TABLE c_lp ADD validity_in_days INT DEFAULT NULL"); } } diff --git a/src/CourseBundle/Entity/CLp.php b/src/CourseBundle/Entity/CLp.php index a43d2bf0f73..16e9debf1f4 100644 --- a/src/CourseBundle/Entity/CLp.php +++ b/src/CourseBundle/Entity/CLp.php @@ -158,9 +158,6 @@ class CLp extends AbstractResource implements ResourceInterface, ResourceShowCou #[ORM\Column(name: 'duration', type: 'integer', nullable: true)] protected ?int $duration = null; - #[ORM\Column(name: 'validity_in_days', type: 'integer', nullable: true)] - protected ?int $validityInDays = null; - public function __construct() { $now = new DateTime(); @@ -650,17 +647,6 @@ public function setDuration(?int $duration): self return $this; } - public function getValidityInDays(): ?int - { - return $this->validityInDays; - } - - public function setValidityInDays(?int $validityInDays): self - { - $this->validityInDays = $validityInDays; - return $this; - } - public function getResourceIdentifier(): int|Uuid { return $this->getIid(); diff --git a/src/CourseBundle/Repository/CLpRepository.php b/src/CourseBundle/Repository/CLpRepository.php index d0a40366ce9..525ed73e748 100644 --- a/src/CourseBundle/Repository/CLpRepository.php +++ b/src/CourseBundle/Repository/CLpRepository.php @@ -138,23 +138,4 @@ public function getLpSessionId(int $lpId): ?int return null; } - - public function findExpiredViews(int $validityDays): array - { - $now = new \DateTime(); - $expirationDate = (clone $now)->modify('-' . $validityDays . ' days'); - - return $this->getEntityManager() - ->createQueryBuilder() - ->select('v') - ->from('Chamilo\CourseBundle\Entity\CLpView', 'v') - ->join('v.lp', 'lp') - ->where('lp.validityInDays > 0') - ->andWhere('v.progress = 100') - ->andWhere('v.session IS NOT NULL') - ->andWhere('v.lastItem < :expirationDate') - ->setParameter('expirationDate', $expirationDate->getTimestamp()) - ->getQuery() - ->getResult(); - } } From 8ca33044e9f0e7d3dcbd127dcd9288110b6b51c0 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Tue, 31 Dec 2024 17:15:13 -0500 Subject: [PATCH 10/11] Internal: Fix gradebook duplication, session course inclusion, query adjustments, and int field handling - refs BT#22057 --- public/main/inc/lib/sessionmanager.lib.php | 2 + .../Command/ReinscriptionCheckCommand.php | 2 +- .../Command/SessionRepetitionCommand.php | 162 ++++++++++++++---- src/CoreBundle/Entity/Session.php | 7 +- .../Repository/SessionRepository.php | 13 +- 5 files changed, 140 insertions(+), 46 deletions(-) diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index baa11b1194b..dd1f0afdacd 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -2880,6 +2880,8 @@ public static function add_courses_to_session( $cat->set_weight(100); $cat->set_visible(0); $cat->set_certificate_min_score(75); + $cat->setGenerateCertificates(1); + $cat->setIsRequirement(1); $cat->add(); $sessionGradeBookCategoryId = $cat->get_id(); } else { diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 7136a0ccbac..96aca79c428 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sessions = $this->sessionRepository->findAll(); foreach ($sessions as $session) { - if ($session->getValidityInDays() === null) { + if ($session->getValidityInDays() === null || $session->getValidityInDays() === 0) { continue; } diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index aee12a1f0d6..de87174ffc7 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -7,9 +7,13 @@ namespace Chamilo\CoreBundle\Command; use Chamilo\CoreBundle\Entity\AccessUrl; +use Chamilo\CoreBundle\Entity\GradebookCategory; +use Chamilo\CoreBundle\Entity\GradebookEvaluation; +use Chamilo\CoreBundle\Entity\GradebookLink; use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Repository\SessionRepository; use Doctrine\ORM\EntityManagerInterface; +use Exception; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -22,22 +26,13 @@ class SessionRepetitionCommand extends Command { protected static $defaultName = 'app:session-repetition'; - private SessionRepository $sessionRepository; - private EntityManagerInterface $entityManager; - private MailerInterface $mailer; - private TranslatorInterface $translator; - public function __construct( - SessionRepository $sessionRepository, - EntityManagerInterface $entityManager, - MailerInterface $mailer, - TranslatorInterface $translator + private readonly SessionRepository $sessionRepository, + private readonly EntityManagerInterface $entityManager, + private readonly MailerInterface $mailer, + private readonly TranslatorInterface $translator ) { parent::__construct(); - $this->sessionRepository = $sessionRepository; - $this->entityManager = $entityManager; - $this->mailer = $mailer; - $this->translator = $translator; } protected function configure(): void @@ -77,6 +72,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Duplicates a session and creates a new session with adjusted dates. + * @throws Exception */ private function duplicateSession(Session $session, bool $debug, OutputInterface $output): Session { @@ -120,19 +116,6 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface $newSession->addAccessUrl($accessUrl->getUrl()); } - // Copy the courses from the original session - foreach ($session->getCourses() as $sessionRelCourse) { - $course = $sessionRelCourse->getCourse(); - if ($course) { - $newSession->addCourse($course); - } - } - - // Copy the general coaches from the original session - foreach ($session->getGeneralCoaches() as $coach) { - $newSession->addGeneralCoach($coach); - } - // Save the new session $this->entityManager->persist($newSession); $this->entityManager->flush(); @@ -141,17 +124,59 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface $output->writeln(sprintf('New session %d created successfully.', $newSession->getId())); } - return $newSession; - } + $courses = $session->getCourses()->toArray(); - /** - * Retrieves or creates a default AccessUrl for sessions. - */ - private function getDefaultAccessUrl() - { - return $this->entityManager->getRepository(AccessUrl::class)->findOneBy([]); - } + if ($debug) { + $output->writeln('Courses retrieved: ' . count($courses)); + foreach ($courses as $index => $sessionRelCourse) { + $course = $sessionRelCourse->getCourse(); + $output->writeln(sprintf( + 'Course #%d: %s (Course ID: %s)', + $index + 1, + $course ? $course->getTitle() : 'NULL', + $course ? $course->getId() : 'NULL' + )); + } + } + + // Extract course IDs + $courseList = array_map(function ($sessionRelCourse) { + $course = $sessionRelCourse->getCourse(); + return $course?->getId(); + }, $courses); + + // Remove null values + $courseList = array_filter($courseList); + + if ($debug) { + $output->writeln(sprintf( + 'Extracted course IDs: %s', + json_encode($courseList) + )); + } + + if (empty($courseList)) { + $output->writeln(sprintf('Warning: No courses found in the original session %d.', $session->getId())); + } + + // Add courses to the new session + foreach ($courses as $sessionRelCourse) { + $course = $sessionRelCourse->getCourse(); + if ($course) { + $newSession->addCourse($course); + + if ($debug) { + $output->writeln(sprintf('Added course ID %d to session ID %d.', $course->getId(), $newSession->getId())); + } + + $this->copyEvaluationsAndCategories($course->getId(), $session->getId(), $newSession->getId(), $debug, $output); + } + } + + $this->entityManager->flush(); + return $newSession; + } /** * Notifies the general coach of the session about the new repetition. @@ -206,4 +231,71 @@ private function generateSessionSummaryLink(Session $session): string { return '/main/session/resume_session.php?id_session=' . $session->getId(); } + + /** + * Copies gradebook categories, evaluations, and links from the old session to the new session. + */ + private function copyEvaluationsAndCategories( + int $courseId, + int $oldSessionId, + int $newSessionId, + bool $debug, + OutputInterface $output + ): void { + // Get existing categories of the original course and session + $categories = $this->entityManager->getRepository(GradebookCategory::class) + ->findBy(['course' => $courseId, 'session' => $oldSessionId]); + + if ($debug) { + $output->writeln(sprintf('Found %d category(ies) for course ID %d in session ID %d.', count($categories), $courseId, $oldSessionId)); + } + + foreach ($categories as $category) { + // Create new category for the new session + $newCategory = new GradebookCategory(); + $newCategory->setTitle($category->getTitle()) + ->setDescription($category->getDescription()) + ->setWeight($category->getWeight()) + ->setVisible($category->getVisible()) + ->setCertifMinScore($category->getCertifMinScore()) + ->setGenerateCertificates($category->getGenerateCertificates()) + ->setIsRequirement($category->getIsRequirement()) + ->setCourse($category->getCourse()) + ->setSession($this->entityManager->getReference(Session::class, $newSessionId)) + ->setParent($category->getParent()); + + $this->entityManager->persist($newCategory); + $this->entityManager->flush(); + + if ($debug) { + $output->writeln(sprintf('Created new category ID %d for session ID %d.', $newCategory->getId(), $newSessionId)); + } + + // Copy links + $links = $this->entityManager->getRepository(GradebookLink::class) + ->findBy(['category' => $category->getId()]); + + foreach ($links as $link) { + $newLink = clone $link; + $newLink->setCategory($newCategory); + $this->entityManager->persist($newLink); + } + + // Copy evaluations + $evaluations = $this->entityManager->getRepository(GradebookEvaluation::class) + ->findBy(['category' => $category->getId()]); + + foreach ($evaluations as $evaluation) { + $newEvaluation = clone $evaluation; + $newEvaluation->setCategory($newCategory); + $this->entityManager->persist($newEvaluation); + } + + $this->entityManager->flush(); + + if ($debug) { + $output->writeln(sprintf('Copied links and evaluations for category ID %d to new category ID %d.', $category->getId(), $newCategory->getId())); + } + } + } } diff --git a/src/CoreBundle/Entity/Session.php b/src/CoreBundle/Entity/Session.php index 494670ebf30..db7740cd4e4 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -1527,7 +1527,7 @@ public function getDaysToReinscription(): ?int public function setDaysToReinscription(?int $daysToReinscription): self { - $this->daysToReinscription = $daysToReinscription; + $this->daysToReinscription = $daysToReinscription ?: null; return $this; } @@ -1551,7 +1551,7 @@ public function getDaysToNewRepetition(): ?int public function setDaysToNewRepetition(?int $daysToNewRepetition): self { - $this->daysToNewRepetition = $daysToNewRepetition; + $this->daysToNewRepetition = $daysToNewRepetition ?: null; return $this; } @@ -1575,7 +1575,8 @@ public function getValidityInDays(): ?int public function setValidityInDays(?int $validityInDays): self { - $this->validityInDays = $validityInDays; + $this->validityInDays = $validityInDays ?: null; + return $this; } } diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index da0ff1de3f9..2b92ef59f1d 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -555,16 +555,15 @@ public function findSessionsWithoutChildAndReadyForRepetition() $currentDate = new \DateTime(); $qb = $this->createQueryBuilder('s') - ->where('s.parentId IS NULL') - ->andWhere('s.daysToNewRepetition IS NOT NULL') + ->where('s.daysToNewRepetition IS NOT NULL') ->andWhere('s.lastRepetition = :false') ->andWhere(':currentDate BETWEEN DATE_SUB(s.accessEndDate, s.daysToNewRepetition, \'DAY\') AND s.accessEndDate') ->andWhere('NOT EXISTS ( - SELECT 1 - FROM Chamilo\CoreBundle\Entity\Session child - WHERE child.parentId = s.id - AND child.accessEndDate >= :currentDate - )') + SELECT 1 + FROM Chamilo\CoreBundle\Entity\Session child + WHERE child.parentId = s.id + AND child.accessEndDate >= :currentDate + )') ->setParameter('false', false) ->setParameter('currentDate', $currentDate); From b21643c713ceb76a6c701a81ac44e1c7bf467e0e Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Mon, 6 Jan 2025 18:37:02 -0500 Subject: [PATCH 11/11] Session: Adjust session-repetition logic and data association - refs BT#22057 --- public/main/gradebook/index.php | 6 ++---- src/CoreBundle/Command/ReinscriptionCheckCommand.php | 7 +++++++ src/CoreBundle/Command/SessionRepetitionCommand.php | 11 +++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/public/main/gradebook/index.php b/public/main/gradebook/index.php index 90c5fc04a17..f1604b66bc9 100644 --- a/public/main/gradebook/index.php +++ b/public/main/gradebook/index.php @@ -827,15 +827,13 @@ function confirmation() { $category = null; if (!empty($selectCat)) { $repo = Container::getGradeBookCategoryRepository(); - if (!empty($categoryId)) { - $category = $repo->find($selectCat); - } + $category = $repo->find($selectCat); $course_id = CourseManager::get_course_by_category($selectCat); $show_message = Category::show_message_resource_delete($course_id); if (empty($show_message)) { // Student if (!api_is_allowed_to_edit() && !api_is_excluded_user_type()) { - if ($category) { + if (null !== $category) { $certificate = Category::generateUserCertificate($category, $stud_id); if ('true' !== $hideCertificateExport && isset($certificate['pdf_url'])) { $actionsLeft .= Display::url( diff --git a/src/CoreBundle/Command/ReinscriptionCheckCommand.php b/src/CoreBundle/Command/ReinscriptionCheckCommand.php index 96aca79c428..e464fb6a817 100644 --- a/src/CoreBundle/Command/ReinscriptionCheckCommand.php +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -226,6 +226,13 @@ private function enrollUserInSession($user, $newSession, $oldSession): void if (!$existingSubscription) { $newSession->addUserInSession(Session::STUDENT, $user); + foreach ($newSession->getCourses() as $sessionRelCourse) { + $course = $sessionRelCourse->getCourse(); + if ($course) { + $newSession->addUserInCourse(Session::STUDENT, $user, $course); + } + } + $subscription = $this->findUserSubscriptionInSession($user, $oldSession); if ($subscription) { $subscription->setNewSubscriptionSessionId($newSession->getId()); diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php index de87174ffc7..bcccad53ab6 100644 --- a/src/CoreBundle/Command/SessionRepetitionCommand.php +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -160,19 +160,30 @@ private function duplicateSession(Session $session, bool $debug, OutputInterface } // Add courses to the new session + $courseCount = 0; foreach ($courses as $sessionRelCourse) { $course = $sessionRelCourse->getCourse(); if ($course) { $newSession->addCourse($course); + $this->entityManager->persist($newSession); if ($debug) { $output->writeln(sprintf('Added course ID %d to session ID %d.', $course->getId(), $newSession->getId())); } $this->copyEvaluationsAndCategories($course->getId(), $session->getId(), $newSession->getId(), $debug, $output); + + $courseCount++; } } + foreach ($session->getGeneralCoaches() as $coach) { + $newSession->addGeneralCoach($coach); + } + + $newSession->setNbrCourses($courseCount); + $this->entityManager->persist($newSession); + $this->entityManager->flush(); return $newSession;