diff --git a/public/main/inc/ajax/model.ajax.php b/public/main/inc/ajax/model.ajax.php index 985fcd64db0..ed4b5c70aaa 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,7 +1982,7 @@ function getWhereClause($col, $oper, $val) break; case 'custom': case 'simple': - case 'all': + case 'replication': $result = SessionManager::getSessionsForAdmin( api_get_user_id(), [ @@ -2000,6 +2001,7 @@ function getWhereClause($col, $oper, $val) break; case 'active': case 'close': + case 'all': $result = SessionManager::formatSessionsAdminForGrid( [ 'where' => $whereCondition, diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index 851c384018a..68f23e8a0bb 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -159,7 +159,12 @@ public static function create_session( $sendSubscriptionNotification = false, $accessUrlId = 0, $status = 0, - $notifyBoss = false + $notifyBoss = false, + $parentId = null, + $daysBeforeFinishingForReinscription = null, + $lastRepetition = false, + $daysBeforeFinishingToCreateNewRepetition = null, + $validityInDays = null ) { global $_configuration; @@ -188,25 +193,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) { @@ -214,16 +211,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; } @@ -239,7 +232,11 @@ public static function create_session( ->setShowDescription(1 === $showDescription) ->setSendSubscriptionNotification((bool) $sendSubscriptionNotification) ->setNotifyBoss((bool) $notifyBoss) - ; + ->setParentId($parentId) + ->setDaysToReinscription((int) $daysBeforeFinishingForReinscription) + ->setLastRepetition($lastRepetition) + ->setDaysToNewRepetition((int) $daysBeforeFinishingToCreateNewRepetition) + ->setValidityInDays((int) $validityInDays); foreach ($coachesId as $coachId) { $session->addGeneralCoach(api_get_user_entity($coachId)); @@ -288,18 +285,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(); @@ -523,6 +508,10 @@ public static function getSessionsForAdmin( } $select .= ', status'; + if ('replication' === $listType) { + $select .= ', parent_id'; + } + if (isset($options['order'])) { $isMakingOrder = 0 === strpos($options['order'], 'category_name'); } @@ -654,6 +643,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; @@ -668,6 +662,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']; @@ -1793,7 +1804,12 @@ public static function edit_session( $sessionAdminId = 0, $sendSubscriptionNotification = false, $status = 0, - $notifyBoss = 0 + $notifyBoss = 0, + $parentId = 0, + $daysBeforeFinishingForReinscription = null, + $daysBeforeFinishingToCreateNewRepetition = null, + $lastRepetition = false, + $validityInDays = null ) { $id = (int) $id; $status = (int) $status; @@ -1863,6 +1879,11 @@ public static function edit_session( ->setVisibility($visibility) ->setSendSubscriptionNotification((bool) $sendSubscriptionNotification) ->setNotifyBoss((bool) $notifyBoss) + ->setParentId($parentId) + ->setDaysToReinscription((int) $daysBeforeFinishingForReinscription) + ->setLastRepetition($lastRepetition) + ->setDaysToNewRepetition((int) $daysBeforeFinishingToCreateNewRepetition) + ->setValidityInDays((int) $validityInDays) ->setAccessStartDate(null) ->setAccessStartDate(null) ->setDisplayStartDate(null) @@ -1871,6 +1892,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 @@ -8271,6 +8302,85 @@ 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('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')) { + $form->addElement( + 'text', + 'days_before_finishing_to_create_new_repetition', + 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')) { + $form->addElement( + 'checkbox', + '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 */ + $element = $form->createElement( + 'select', + 'parent_id', + get_lang('Parent session'), + [], + ['class' => 'form-control'] + ); + + $element->addOption(get_lang('None'), 0, []); + $sessions = SessionManager::getListOfParentSessions(); + $currentSessionId = $session?->getId(); + foreach ($sessions as $id => $title) { + if ($id !== $currentSessionId) { + $attributes = []; + $element->addOption($title, $id, $attributes); + } + } + + $form->addElement($element); + $form->addElement('html', ''); $js = $extra['jquery_ready_content']; @@ -8791,7 +8901,7 @@ public static function getGridColumns( ]; break; - + case 'replication': case 'custom': $columns = [ '#', @@ -8810,6 +8920,7 @@ public static function getGridColumns( [ 'name' => 'title', 'index' => 's.title', + 'width' => '260px', 'width' => '300', 'align' => 'left', 'search' => 'true', @@ -9805,9 +9916,9 @@ public static function getDefaultSessionTab() } /** - * @return array + * @return string */ - public static function getSessionListTabs($listType) + public static function getSessionListTabs($listType): string { $tabs = [ [ @@ -9826,10 +9937,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) { @@ -9845,6 +9956,9 @@ public static function getSessionListTabs($listType) case 'custom': $default = 4; break; + case 'replication': + $default = 5; + break; } return Display::tabsOnlyLink($tabs, $default); @@ -10227,6 +10341,24 @@ 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; + } + + /** * Method to export sessions data as CSV */ @@ -10413,5 +10545,4 @@ private static function generateSessionCourseReportData($sessionId, $courseId, $ return [$csvHeaders, $csvContent]; } - } diff --git a/public/main/session/session_add.php b/public/main/session/session_add.php index eccd3bb61cc..309b94170c6 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 @@ -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) { @@ -245,10 +246,22 @@ function (User $user) { $session->getGeneralCoaches()->getValues() ), 'session_template' => $session->getTitle(), + '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, ]; + + 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; + } } } @@ -261,15 +274,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']); @@ -311,6 +321,13 @@ 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; + $validityInDays = $params['validity_in_days'] ?? null; $return = SessionManager::create_session( $title, @@ -328,11 +345,16 @@ function (User $user) { $description, $showDescription, $extraFields, - null, + 0, $sendSubscriptionNotification, api_get_current_access_url_id(), $status, - $notifyBoss + $notifyBoss, + $parentId, + $daysBeforeFinishingForReinscription, + $lastRepetition, + $daysBeforeFinishingToCreateNewRepetition, + $validityInDays ); if ($return == strval(intval($return))) { diff --git a/public/main/session/session_edit.php b/public/main/session/session_edit.php index 0c293604ebd..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(), @@ -72,8 +73,16 @@ 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, ]; +if ($showValidityField) { + $formDefaults['validity_in_days'] = $session->getValidityInDays(); +} + $form->setDefaults($formDefaults); if ($form->validate()) { @@ -113,6 +122,12 @@ function (User $user) { $status = $params['status'] ?? 0; $notifyBoss = isset($params['notify_boss']) ? 1 : 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']); + $validityInDays = $params['validity_in_days'] ?? null; + $return = SessionManager::edit_session( $id, $name, @@ -132,7 +147,12 @@ function (User $user) { null, $sendSubscriptionNotification, $status, - $notifyBoss + $notifyBoss, + $parentId, + $daysBeforeFinishingForReinscription, + $daysBeforeFinishingToCreateNewRepetition, + $lastRepetition, + $validityInDays ); if ($return) { diff --git a/public/main/session/session_list.php b/public/main/session/session_list.php index 1ebe175b831..19312887af1 100644 --- a/public/main/session/session_list.php +++ b/public/main/session/session_list.php @@ -133,11 +133,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..7136a0ccbac --- /dev/null +++ b/src/CoreBundle/Command/ReinscriptionCheckCommand.php @@ -0,0 +1,283 @@ +sessionRepository = $sessionRepository; + $this->certificateRepository = $certificateRepository; + $this->entityManager = $entityManager; + } + + protected function configure(): void + { + $this + ->setDescription('Checks for users who have validated all gradebooks 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'); + + $sessions = $this->sessionRepository->findAll(); + + foreach ($sessions as $session) { + if ($session->getValidityInDays() === null) { + continue; + } + + $users = $this->getUsersForSession($session); + + foreach ($users as $user) { + if ($debug) { + $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() + )); + } + } + } + } + } + + 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(); + + $studentSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT); + foreach ($studentSubscriptions as $studentSubscription) { + $usersToNotify[$studentSubscription->getUser()->getId()] = $studentSubscription->getUser(); + } + + $coachSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::COURSE_COACH); + foreach ($coachSubscriptions as $coachSubscription) { + $usersToNotify[$coachSubscription->getUser()->getId()] = $coachSubscription->getUser(); + } + } + + $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 false; + } + + /** + * Gets the user's certificates for the courses in the session. + */ + private function getUserCertificatesForSession($user, Session $session): array + { + $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(); + } + + /** + * 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 getLatestCertificateDate(array $certificates): ?\DateTime + { + $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) { + $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) + ->findOneBy([ + 'user' => $user, + 'session' => $session, + ]); + } + + /** + * Finds a valid session within the session hierarchy. + */ + 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; + } + } + + $parentSession = $this->sessionRepository->findParentSession($session); + + if ($parentSession && new \DateTime() <= $parentSession->getAccessEndDate()) { + return $parentSession; + } + + return null; + } +} diff --git a/src/CoreBundle/Command/SessionRepetitionCommand.php b/src/CoreBundle/Command/SessionRepetitionCommand.php new file mode 100644 index 00000000000..aee12a1f0d6 --- /dev/null +++ b/src/CoreBundle/Command/SessionRepetitionCommand.php @@ -0,0 +1,209 @@ +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())->days; + $newStartDate = (clone $session->getAccessEndDate())->modify('+1 day'); + $newEndDate = (clone $newStartDate)->modify("+{$duration} days"); + + 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() . ' - ' . time() . ')') + ->setAccessStartDate($newStartDate) + ->setAccessEndDate($newEndDate) + ->setDisplayStartDate($newStartDate) + ->setDisplayEndDate($newEndDate) + ->setCoachAccessStartDate($newStartDate) + ->setCoachAccessEndDate($newEndDate) + ->setVisibility($session->getVisibility()) + ->setDuration(0) + ->setDescription($session->getDescription() ?? '') + ->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 + foreach ($session->getUrls() as $accessUrl) { + $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(); + + 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/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 0bda39bde58..4185ef89371 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -380,9 +380,25 @@ 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; + #[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(); @@ -446,7 +462,7 @@ public function setDuration(int $duration): self public function getShowDescription(): bool { - return $this->showDescription; + return $this->showDescription ?? false; } public function setShowDescription(bool $showDescription): self @@ -1474,6 +1490,54 @@ public function getClosedOrHiddenCourses(): Collection )); } + 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; + } + public function getNotifyBoss(): bool { return $this->notifyBoss; @@ -1485,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 new file mode 100644 index 00000000000..4faec2e633d --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240928003000.php @@ -0,0 +1,139 @@ +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'); + + 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 '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 DROP COLUMN validity_in_days"); + } + } + + // 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 + { + $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'); + + 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 '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 ADD validity_in_days INT DEFAULT NULL"); + } + } + + // Remove settings + $this->addSql("DELETE FROM settings WHERE variable = 'enable_auto_reinscription'"); + $this->addSql("DELETE FROM settings WHERE variable = 'enable_session_replication'"); + } +} diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index 4efc2b93acd..da0ff1de3f9 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -464,6 +464,113 @@ 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); + $now = new \DateTime(); + + foreach ($childSessions as $childSession) { + $startDate = $childSession->getAccessStartDate(); + $endDate = $childSession->getAccessEndDate(); + $daysToReinscription = $childSession->getDaysToReinscription(); + + if (empty($daysToReinscription) || $daysToReinscription <= 0) { + continue; + } + + $adjustedEndDate = (clone $endDate)->modify('-' . $daysToReinscription . ' days'); + + if ($startDate <= $now && $adjustedEndDate >= $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') + ->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); + + return $qb->getQuery()->getResult(); + } + public function countUsersBySession(int $sessionId, int $relationType = Session::STUDENT): int { $qb = $this->createQueryBuilder('s'); 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); diff --git a/src/CourseBundle/Repository/CLpRepository.php b/src/CourseBundle/Repository/CLpRepository.php index affb60f5666..525ed73e748 100644 --- a/src/CourseBundle/Repository/CLpRepository.php +++ b/src/CourseBundle/Repository/CLpRepository.php @@ -117,4 +117,25 @@ 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; + } }