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 eff09bb834c..0a73af77148 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -159,7 +159,11 @@ public static function create_session( $sendSubscriptionNotification = false, $accessUrlId = 0, $status = 0, - $notifyBoss = false + $notifyBoss = false, + $parentId = null, + $daysBeforeFinishingForReinscription = null, + $lastRepetition = false, + $daysBeforeFinishingToCreateNewRepetition = null ) { global $_configuration; @@ -188,25 +192,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 +210,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 +231,10 @@ public static function create_session( ->setShowDescription(1 === $showDescription) ->setSendSubscriptionNotification((bool) $sendSubscriptionNotification) ->setNotifyBoss((bool) $notifyBoss) - ; + ->setParentId($parentId) + ->setDaysToReinscription($daysBeforeFinishingForReinscription) + ->setLastRepetition($lastRepetition) + ->setDaysToNewRepetition($daysBeforeFinishingToCreateNewRepetition); foreach ($coachesId as $coachId) { $session->addGeneralCoach(api_get_user_entity($coachId)); @@ -288,18 +283,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 +506,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 +641,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 +660,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 +1802,11 @@ public static function edit_session( $sessionAdminId = 0, $sendSubscriptionNotification = false, $status = 0, - $notifyBoss = 0 + $notifyBoss = 0, + $parentId = 0, + $daysBeforeFinishingForReinscription = null, + $daysBeforeFinishingToCreateNewRepetition = null, + $lastRepetition = false ) { $id = (int) $id; $status = (int) $status; @@ -1863,6 +1876,10 @@ public static function edit_session( ->setVisibility($visibility) ->setSendSubscriptionNotification((bool) $sendSubscriptionNotification) ->setNotifyBoss((bool) $notifyBoss) + ->setParentId($parentId) + ->setDaysToReinscription($daysBeforeFinishingForReinscription) + ->setLastRepetition($lastRepetition) + ->setDaysToNewRepetition($daysBeforeFinishingToCreateNewRepetition) ->setAccessStartDate(null) ->setAccessStartDate(null) ->setDisplayStartDate(null) @@ -1871,6 +1888,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 +8298,50 @@ 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] + ); + } + + 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] + ); + } + + 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') + ); + } + + /** @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(); + foreach ($sessions as $id => $title) { + $attributes = []; + $element->addOption($title, $id, $attributes); + } + + $form->addElement($element); + $form->addElement('html', ''); $js = $extra['jquery_ready_content']; @@ -8782,7 +8853,7 @@ public static function getGridColumns( ]; break; - + case 'replication': case 'custom': $columns = [ '#', @@ -8801,7 +8872,7 @@ public static function getGridColumns( [ 'name' => 'title', 'index' => 's.title', - 'width' => '160', + 'width' => '260px', 'align' => 'left', 'search' => 'true', 'searchoptions' => ['sopt' => $operators], @@ -9792,9 +9863,9 @@ public static function getDefaultSessionTab() } /** - * @return array + * @return string */ - public static function getSessionListTabs($listType) + public static function getSessionListTabs($listType): string { $tabs = [ [ @@ -9813,10 +9884,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) { @@ -9832,6 +9903,9 @@ public static function getSessionListTabs($listType) case 'custom': $default = 4; break; + case 'replication': + $default = 5; + break; } return Display::tabsOnlyLink($tabs, $default); @@ -10213,4 +10287,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 eccd3bb61cc..36ad63d7ab2 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 @@ -245,6 +245,10 @@ 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, ]; } else { $formDefaults['access_start_date'] = $formDefaults['display_start_date'] = api_get_local_time(); @@ -261,15 +265,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 +312,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, @@ -328,11 +335,15 @@ function (User $user) { $description, $showDescription, $extraFields, - null, + 0, $sendSubscriptionNotification, api_get_current_access_url_id(), $status, - $notifyBoss + $notifyBoss, + $parentId, + $daysBeforeFinishingForReinscription, + $lastRepetition, + $daysBeforeFinishingToCreateNewRepetition ); if ($return == strval(intval($return))) { diff --git a/public/main/session/session_edit.php b/public/main/session/session_edit.php index 0c293604ebd..77fa8d2ba53 100644 --- a/public/main/session/session_edit.php +++ b/public/main/session/session_edit.php @@ -72,6 +72,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); @@ -113,6 +117,11 @@ 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']); + $return = SessionManager::edit_session( $id, $name, @@ -132,7 +141,11 @@ function (User $user) { null, $sendSubscriptionNotification, $status, - $notifyBoss + $notifyBoss, + $parentId, + $daysBeforeFinishingForReinscription, + $daysBeforeFinishingToCreateNewRepetition, + $lastRepetition ); if ($return) { 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..ccf6eddcd82 --- /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() . ' - ' . time() . ')') + ->setAccessStartDate($newStartDate) + ->setAccessEndDate($newEndDate) + ->setDisplayStartDate($newStartDate) + ->setDisplayEndDate($newEndDate) + ->setCoachAccessStartDate($newStartDate) + ->setCoachAccessEndDate($newEndDate) + ->setVisibility($session->getVisibility()) + ->setDuration($session->getDuration()) + ->setDescription($session->getDescription() ?? '') + ->setShowDescription($session->getShowDescription() ?? false) + ->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/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 e6c97093afa..309c7bf4efa 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; + #[ORM\Column(name: 'notify_boss', type: 'boolean', options: ['default' => false])] protected bool $notifyBoss = false; @@ -440,7 +452,7 @@ public function setDuration(int $duration): self public function getShowDescription(): bool { - return $this->showDescription; + return $this->showDescription ?? false; } public function setShowDescription(bool $showDescription): self @@ -1452,6 +1464,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; 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/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/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(); + } }