|
23 | 23 | </style>'; |
24 | 24 |
|
25 | 25 | $export = isset($_GET['export']) ? $_GET['export'] : false; |
26 | | -$sessionId = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0; |
27 | | -$action = isset($_GET['action']) ? $_GET['action'] : ''; |
| 26 | +$sessionId = isset($_GET['id_session']) ? (int) $_GET['id_session'] : (isset($_POST['id_session']) ? (int) $_POST['id_session'] : 0); |
| 27 | +$action = isset($_GET['action']) ? $_GET['action'] : (isset($_POST['action']) ? $_POST['action'] : ''); |
28 | 28 | $origin = api_get_origin(); |
29 | 29 | $course_code = isset($_GET['course']) ? Security::remove_XSS($_GET['course']) : ''; |
30 | 30 | $courseInfo = api_get_course_info($course_code); |
31 | 31 | $courseCode = ''; |
32 | 32 | if ($courseInfo) { |
33 | 33 | $courseCode = $courseInfo['code']; |
34 | 34 | } |
35 | | -$student_id = isset($_GET['student']) ? (int) $_GET['student'] : 0; |
| 35 | +$student_id = isset($_GET['student']) ? (int) $_GET['student'] : (isset($_POST['student']) ? (int) $_POST['student'] : 0); |
36 | 36 | $coachId = isset($_GET['id_coach']) ? (int) $_GET['id_coach'] : 0; |
37 | 37 | $details = isset($_GET['details']) ? Security::remove_XSS($_GET['details']) : ''; |
38 | 38 | $currentUrl = api_get_self().'?student='.$student_id.'&course='.$courseCode.'&id_session='.$sessionId |
|
152 | 152 | if (false === $subscriptionColumnEnabled) { |
153 | 153 | break; |
154 | 154 | } |
| 155 | + $canManageSubscriptions = api_is_platform_admin(true, true) |
| 156 | + || api_is_session_admin() |
| 157 | + || api_is_allowed_to_edit(null, true) |
| 158 | + || api_is_course_admin() |
| 159 | + || api_is_teacher() |
| 160 | + || api_is_coach(); |
| 161 | + if (!$canManageSubscriptions) { |
| 162 | + api_not_allowed(true); |
| 163 | + } |
155 | 164 | $courseCodeParam = isset($_GET['course_code']) ? Security::remove_XSS($_GET['course_code']) : ''; |
156 | 165 | $sessionParam = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0; |
157 | 166 | if ('' !== $courseCodeParam && $sessionParam > 0) { |
|
174 | 183 | if (false === $subscriptionColumnEnabled) { |
175 | 184 | break; |
176 | 185 | } |
| 186 | + $canManageSubscriptions = api_is_platform_admin(true, true) |
| 187 | + || api_is_session_admin() |
| 188 | + || api_is_allowed_to_edit(null, true) |
| 189 | + || api_is_course_admin() |
| 190 | + || api_is_teacher() |
| 191 | + || api_is_coach(); |
| 192 | + if (!$canManageSubscriptions) { |
| 193 | + api_not_allowed(true); |
| 194 | + } |
177 | 195 | $courseCodeParam = isset($_GET['course_code']) ? Security::remove_XSS($_GET['course_code']) : ''; |
178 | 196 | $sessionParam = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0; |
179 | 197 | if ('' !== $courseCodeParam && $sessionParam > 0) { |
|
193 | 211 | ]); |
194 | 212 | header('Location: '.$redirectUrl); |
195 | 213 | exit; |
| 214 | + case 'bulk_session_course_subscription': |
| 215 | + // Bulk subscribe/unsubscribe selected courses within a session |
| 216 | + if (false === $subscriptionColumnEnabled) { |
| 217 | + break; |
| 218 | + } |
| 219 | + if (!Security::check_token('post')) { |
| 220 | + api_not_allowed(true); |
| 221 | + } |
| 222 | + $canManageSubscriptions = api_is_platform_admin(true, true) |
| 223 | + || api_is_session_admin() |
| 224 | + || api_is_allowed_to_edit(null, true) |
| 225 | + || api_is_course_admin() |
| 226 | + || api_is_teacher() |
| 227 | + || api_is_coach(); |
| 228 | + if (!$canManageSubscriptions) { |
| 229 | + api_not_allowed(true); |
| 230 | + } |
| 231 | + $sessionParam = isset($_POST['id_session']) ? (int) $_POST['id_session'] : 0; |
| 232 | + $courseCodes = isset($_POST['course_codes']) && is_array($_POST['course_codes']) ? $_POST['course_codes'] : []; |
| 233 | + $bulkAction = isset($_POST['bulk_action']) ? Security::remove_XSS($_POST['bulk_action']) : ''; |
| 234 | + if ($sessionParam > 0 && !empty($courseCodes) && in_array($bulkAction, ['subscribe', 'unsubscribe'], true)) { |
| 235 | + if ($bulkAction === 'subscribe') { |
| 236 | + foreach ($courseCodes as $cc) { |
| 237 | + $cc = Security::remove_XSS($cc); |
| 238 | + if (!empty($cc)) { |
| 239 | + SessionManager::subscribe_users_to_session_course([$student_id], $sessionParam, $cc); |
| 240 | + } |
| 241 | + } |
| 242 | + } else { // unsubscribe |
| 243 | + foreach ($courseCodes as $cc) { |
| 244 | + $cc = Security::remove_XSS($cc); |
| 245 | + if (!empty($cc)) { |
| 246 | + $ci = api_get_course_info($cc); |
| 247 | + SessionManager::removeUsersFromCourseSession([$student_id], $sessionParam, $ci); |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + Display::addFlash(Display::return_message(get_lang('Updated'))); |
| 252 | + Security::clear_token(); |
| 253 | + } else { |
| 254 | + Display::addFlash(Display::return_message(get_lang('NoItemSelected'), 'warning')); |
| 255 | + } |
| 256 | + $redirectUrl = api_get_self().'?' . http_build_query([ |
| 257 | + 'student' => $student_id, |
| 258 | + 'origin' => $origin, |
| 259 | + 'details' => $details, |
| 260 | + 'id_session' => $sessionParam, |
| 261 | + ]); |
| 262 | + header('Location: '.$redirectUrl); |
| 263 | + exit; |
196 | 264 | case 'export_one_session_row': |
197 | 265 | $sessionToExport = isset($_GET['session_to_export']) ? (int) $_GET['session_to_export'] : 0; |
198 | 266 | $exportList = Session::read('export_course_list'); |
|
705 | 773 | } |
706 | 774 |
|
707 | 775 | $sessionTable = Database::get_main_table(TABLE_MAIN_SESSION); |
| 776 | +$sessionCourseTable = Database::get_main_table(TABLE_MAIN_SESSION_COURSE); |
| 777 | +$courseTable = Database::get_main_table(TABLE_MAIN_COURSE); |
708 | 778 |
|
709 | | -$sessionPositionOrder = ''; |
| 779 | +// Determine order like before for sessions |
710 | 780 | $allowOrder = api_get_configuration_value('session_list_order'); |
711 | | -if ($allowOrder) { |
712 | | - $sessionPositionOrder = 's.position ASC, '; |
713 | | -} |
| 781 | +$orderCondition = $allowOrder ? ' ORDER BY s.position ASC, s.display_end_date DESC' : ' ORDER BY s.display_end_date DESC'; |
| 782 | + |
| 783 | +// Use core helper to fetch sessions followed by user with the expected order |
| 784 | +$sessionsForStudent = SessionManager::getSessionsFollowedByUser( |
| 785 | + $student_id, |
| 786 | + null, |
| 787 | + null, |
| 788 | + null, |
| 789 | + false, |
| 790 | + false, |
| 791 | + false, |
| 792 | + $orderCondition |
| 793 | +); |
714 | 794 |
|
715 | | -// Get the list of sessions where the user is subscribed as student |
716 | | -$sql = 'SELECT DISTINCT sc.session_id, sc.c_id |
717 | | - FROM '.Database::get_main_table(TABLE_MAIN_SESSION_COURSE).' sc |
718 | | - INNER JOIN '.$sessionTable.' as s |
719 | | - ON (s.id = sc.session_id) |
720 | | - INNER JOIN '.Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER).' as scu |
721 | | - ON (scu.session_id = sc.session_id) |
722 | | - WHERE s.id = scu.session_id |
723 | | - AND user_id = '.$student_id.' |
724 | | - ORDER BY '.$sessionPositionOrder.'display_end_date DESC, sc.position ASC |
725 | | - '; |
726 | | -$rs = Database::query($sql); |
727 | 795 | $tmp_sessions = []; |
728 | | -while ($row = Database::fetch_array($rs, 'ASSOC')) { |
729 | | - $tmp_sessions[] = $row['session_id']; |
730 | | - if ($drh_can_access_all_courses) { |
731 | | - if (in_array($row['session_id'], $tmp_sessions)) { |
732 | | - $courses_in_session[$row['session_id']][] = $row['c_id']; |
733 | | - } |
734 | | - } else { |
735 | | - if (isset($courses_in_session_by_coach[$row['session_id']])) { |
736 | | - if (in_array($row['session_id'], $tmp_sessions)) { |
737 | | - $courses_in_session[$row['session_id']][] = $row['c_id']; |
738 | | - } |
| 796 | +foreach ($sessionsForStudent as $sessionItem) { |
| 797 | + $sid = (int) $sessionItem['id']; |
| 798 | + $tmp_sessions[] = $sid; |
| 799 | + |
| 800 | + // Fetch all courses of the session ordered by sc.position ASC |
| 801 | + $sqlCourses = 'SELECT sc.c_id, sc.position, c.code |
| 802 | + FROM '.$sessionCourseTable.' sc |
| 803 | + INNER JOIN '.$courseTable.' c ON (c.id = sc.c_id) |
| 804 | + WHERE sc.session_id = '.$sid.' |
| 805 | + ORDER BY sc.position ASC'; |
| 806 | + $rsCourses = Database::query($sqlCourses); |
| 807 | + $cidList = []; |
| 808 | + while ($rowC = Database::fetch_array($rsCourses, 'ASSOC')) { |
| 809 | + $code = $rowC['code']; |
| 810 | + // Respect coach restrictions when applicable |
| 811 | + if ($drh_can_access_all_courses || !isset($courses_in_session_by_coach[$sid]) || isset($courses_in_session_by_coach[$sid][$code])) { |
| 812 | + $cidList[] = (int) $rowC['c_id']; |
739 | 813 | } |
740 | 814 | } |
| 815 | + // Always define the session key, even if empty, to render |
| 816 | + $courses_in_session[$sid] = $cidList; |
741 | 817 | } |
742 | 818 |
|
743 | 819 | $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(api_get_user_id(), $courseInfo); |
|
1517 | 1593 | .' '.$session_name.($date_session ? ' ('.$date_session.')' : ''); |
1518 | 1594 | } |
1519 | 1595 |
|
| 1596 | + // Permission for subscription actions (admin, session admin, teacher) |
| 1597 | + $canManageSubscriptions = api_is_platform_admin(true, true) |
| 1598 | + || api_is_session_admin() |
| 1599 | + || api_is_allowed_to_edit(null, true) |
| 1600 | + || api_is_course_admin() |
| 1601 | + || api_is_teacher() |
| 1602 | + || api_is_coach(); |
| 1603 | + |
1520 | 1604 | // Courses |
1521 | 1605 | echo '<h3>'.$title.'</h3>'; |
| 1606 | + |
| 1607 | + // Determine columns count dynamically (including optional columns) |
| 1608 | + $hasBulk = ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions); |
| 1609 | + $columnsCount = 0; |
| 1610 | + // Checkbox bulk column |
| 1611 | + if ($hasBulk) { $columnsCount += 1; } |
| 1612 | + // Base columns |
| 1613 | + $columnsCount += 4; // Course, Time, Progress, Score |
| 1614 | + // Theoretical time |
| 1615 | + if ($theoreticalTimeEnabled) { $columnsCount += 1; } |
| 1616 | + // Subscription icon column (single action) |
| 1617 | + if ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions) { $columnsCount += 1; } |
| 1618 | + // Attendances, Evaluations, Details |
| 1619 | + $columnsCount += 3; |
| 1620 | + |
| 1621 | + echo '<form method="post" action="'.api_get_self().'">'; |
1522 | 1622 | echo '<div class="table-responsive">'; |
1523 | 1623 | echo '<table class="table table-striped table-hover courses-tracking">'; |
1524 | 1624 | echo '<thead>'; |
1525 | | - echo '<tr> |
1526 | | - <th>'.get_lang('Course').'</th> |
1527 | | - <th>'.get_lang('Time').'</th> |
1528 | | - <th>'.get_lang('Progress').' '.Display::return_icon('info3.gif', get_lang('progressBasedOnVisiblesLPsInEachCourse'), [], ICON_SIZE_TINY).' </th> |
1529 | | - <th>'.get_lang('Score').'</th>'; |
1530 | | - if($theoreticalTimeEnabled) { |
| 1625 | + echo '<tr>'; |
| 1626 | + if ($hasBulk) { |
| 1627 | + // Select all for this session's courses |
| 1628 | + $selectAllId = 'bulk-select-all-s'.$sId; |
| 1629 | + echo '<th style="width:20px;">' |
| 1630 | + .'<input type="checkbox" id="'.$selectAllId.'" class="bulk-select-all" data-target="s'.$sId.'" />' |
| 1631 | + .'</th>'; |
| 1632 | + } |
| 1633 | + echo '<th>'.get_lang('Course').'</th>'; |
| 1634 | + echo '<th>'.get_lang('Time').'</th>'; |
| 1635 | + echo '<th>'.get_lang('Progress').' '.Display::return_icon('info3.gif', get_lang('progressBasedOnVisiblesLPsInEachCourse'), [], ICON_SIZE_TINY).' </th>'; |
| 1636 | + echo '<th>'.get_lang('Score').'</th>'; |
| 1637 | + if ($theoreticalTimeEnabled) { |
1531 | 1638 | echo '<th>'.get_lang('TheoreticalTime').'</th>'; |
1532 | 1639 | } |
1533 | | - if ($subscriptionColumnEnabled && !empty($sId)) { |
| 1640 | + if ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions) { |
1534 | 1641 | echo '<th>'.get_lang('Subscription').'</th>'; |
1535 | 1642 | } |
1536 | | - echo '<th>'.get_lang('AttendancesFaults').'</th> |
1537 | | - <th>'.get_lang('Evaluations').'</th> |
1538 | | - <th>'.get_lang('Details').'</th> |
1539 | | - </tr>'; |
| 1643 | + echo '<th>'.get_lang('AttendancesFaults').'</th>'; |
| 1644 | + echo '<th>'.get_lang('Evaluations').'</th>'; |
| 1645 | + echo '<th>'.get_lang('Details').'</th>'; |
| 1646 | + echo '</tr>'; |
1540 | 1647 | echo '</thead>'; |
1541 | 1648 | echo '<tbody>'; |
1542 | 1649 |
|
|
1609 | 1716 | $score = '0%'; |
1610 | 1717 | $subscriptionIcon = ''; |
1611 | 1718 | $subscriptionCsv = ''; |
1612 | | - if ($subscriptionColumnEnabled && !empty($sId)) { |
| 1719 | + if ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions) { |
1613 | 1720 | $subscribeUrl = api_get_self().'?' . http_build_query([ |
1614 | 1721 | 'action' => 'subscribe_course', |
1615 | 1722 | 'id_session' => $sId, |
|
1647 | 1754 | } |
1648 | 1755 |
|
1649 | 1756 | if ($isSubscribed) { |
1650 | | - if ($subscriptionColumnEnabled && !empty($sId)) { |
| 1757 | + if ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions) { |
1651 | 1758 | $subscriptionIcon = Display::url( |
1652 | 1759 | Display::return_icon('delete.png', get_lang('Registered')), |
1653 | 1760 | $unsubscribeUrl |
|
1773 | 1880 |
|
1774 | 1881 | $csv_content[] = $csvRow; |
1775 | 1882 | $exportCourseList[$sId][] = $csvRow; |
1776 | | - $rowClass = $isSubscribed ? '' : ' class="course-unsubscribed"'; |
1777 | | - echo '<tr'.$rowClass.'>', |
| 1883 | + $rowClass = (!$isSubscribed && $subscriptionColumnEnabled) ? ' class="course-unsubscribed"' : ''; |
| 1884 | + echo '<tr'.$rowClass.'>'; |
| 1885 | + if ($hasBulk) { |
| 1886 | + echo '<td>' |
| 1887 | + .'<input type="checkbox" name="course_codes[]" value="'.Security::remove_XSS($courseCodeItem).'"' |
| 1888 | + .' class="bulk-course-checkbox bulk-course-checkbox-s'.$sId.'" />' |
| 1889 | + .'</td>'; |
| 1890 | + } |
| 1891 | + echo |
1778 | 1892 | '<td>', |
1779 | 1893 | '<a href="'.$courseInfoItem['course_public_url'].'?id_session='.$sId.'">'. |
1780 | 1894 | $courseInfoItem['title']. |
|
1786 | 1900 | if($theoreticalTimeEnabled) { |
1787 | 1901 | echo '<td>'.$theoreticalTimeDisplay.'</td>'; |
1788 | 1902 | } |
1789 | | - if ($subscriptionColumnEnabled && !empty($sId)) { |
| 1903 | + if ($subscriptionColumnEnabled && !empty($sId) && $canManageSubscriptions) { |
1790 | 1904 | echo '<td>'.$subscriptionIcon.'</td>'; |
1791 | 1905 | } |
1792 | 1906 | echo '<td>'.$attendances_faults_avg.'</td>', |
|
1815 | 1929 | ); |
1816 | 1930 | $totalEvaluations = $scoreDisplay->display_score($gradeBookTotal); |
1817 | 1931 | $totalTimeFormatted = api_time_to_hms($totalCourseTime); |
1818 | | - echo '<tr> |
1819 | | - <th>'.get_lang('Total').'</th> |
| 1932 | + echo '<tr>'; |
| 1933 | + if ($hasBulk) { |
| 1934 | + echo '<th></th>'; |
| 1935 | + } |
| 1936 | + echo '<th>'.get_lang('Total').'</th> |
1820 | 1937 | <th>'.$totalTimeFormatted.'</th> |
1821 | 1938 | <th>'.$totalProgressFormatted.'</th> |
1822 | 1939 | <th>'.$totalScoreFormatted.'</th>'; |
|
1968 | 2085 | } |
1969 | 2086 | echo $sessionAction; |
1970 | 2087 | } else { |
1971 | | - echo "<tr><td colspan='5'>".get_lang('NoCourse')."</td></tr>"; |
| 2088 | + echo "<tr><td colspan='".$columnsCount."'>".get_lang('NoCourse')."</td></tr>"; |
1972 | 2089 | } |
1973 | 2090 | Session::write('export_course_list', $exportCourseList); |
1974 | 2091 | echo '</tbody>'; |
1975 | 2092 | echo '</table>'; |
1976 | 2093 | echo '</div>'; |
| 2094 | + if ($hasBulk) { |
| 2095 | + // Bulk action controls per session |
| 2096 | + echo '<div class="row" style="margin:10px 0;">' |
| 2097 | + .'<div class="col-sm-12">' |
| 2098 | + .'<div class="form-inline">' |
| 2099 | + .'<input type="hidden" name="action" value="bulk_session_course_subscription" />' |
| 2100 | + .'<input type="hidden" name="id_session" value="'.$sId.'" />' |
| 2101 | + .'<input type="hidden" name="student" value="'.$student_id.'" />' |
| 2102 | + .'<input type="hidden" name="origin" value="'.Security::remove_XSS($origin).'" />' |
| 2103 | + .'<input type="hidden" name="details" value="'.Security::remove_XSS($details).'" />' |
| 2104 | + .'<input type="hidden" name="sec_token" value="'.$token.'" />' |
| 2105 | + .'<label class="control-label" for="bulk-action-'.$sId.'" style="margin-right:8px;">'.get_lang('Action').'</label>' |
| 2106 | + .'<select id="bulk-action-'.$sId.'" name="bulk_action" class="form-control" style="margin-right:8px;">' |
| 2107 | + .'<option value="subscribe">'.get_lang('Subscribe').'</option>' |
| 2108 | + .'<option value="unsubscribe">'.get_lang('Unsubscribe').'</option>' |
| 2109 | + .'</select>' |
| 2110 | + .'<button type="submit" class="btn btn-primary">'.get_lang('Validate').'</button>' |
| 2111 | + .'</div>' |
| 2112 | + .'</div>' |
| 2113 | + .'</div>'; |
| 2114 | + |
| 2115 | + // Small JS to toggle all checkboxes per session |
| 2116 | + echo "<script>\n". |
| 2117 | + "(function(){\n". |
| 2118 | + " var el = document.getElementById('".$selectAllId."');\n". |
| 2119 | + " if (el) {\n". |
| 2120 | + " el.addEventListener('change', function(){\n". |
| 2121 | + " var target = this.getAttribute('data-target');\n". |
| 2122 | + " var boxes = document.querySelectorAll('.bulk-course-checkbox-' + target);\n". |
| 2123 | + " for (var i=0;i<boxes.length;i++){ boxes[i].checked = this.checked; }\n". |
| 2124 | + " });\n". |
| 2125 | + " }\n". |
| 2126 | + "})();\n". |
| 2127 | + "</script>"; |
| 2128 | + } |
| 2129 | + echo '</form>'; |
1977 | 2130 | } |
1978 | 2131 | } else { |
1979 | 2132 | $columnHeaders = [ |
|
0 commit comments