Browse Source

Minor - add from 1.11.x

Julio Montoya 5 years ago
parent
commit
cccadff062

+ 96 - 0
main/admin/periodic_export.php

@@ -0,0 +1,96 @@
+<?php
+/* For licensing terms, see /license.txt */
+exit;
+$cidReset = true;
+require_once __DIR__.'/../inc/global.inc.php';
+
+api_protect_admin_script(true);
+api_set_more_memory_and_time_limits();
+
+$this_section = SECTION_PLATFORM_ADMIN;
+$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('PlatformAdmin')];
+
+$nameTools = get_lang('PeriodicExports');
+$export = '';
+
+Display::display_header($nameTools);
+
+echo Display::page_header($nameTools);
+
+$form = new FormValidator('special_exports', 'post');
+$form->addDateRangePicker('date', get_lang('Dates'));
+$form->addButtonSearch(get_lang('Search'));
+
+$form->display();
+
+if ($form->validate()) {
+    $values = $form->getSubmitValues();
+    $urlId = api_get_current_access_url_id();
+    $startDate = $values['date_start'];
+    $endDate = $values['date_end'];
+
+    // Count active users in the platform
+    $countUsers = UserManager::get_number_of_users(null, $urlId, 1);
+    // Count user connected in those dates
+    $connectedUsers = Statistics::getLoginCount($startDate, $endDate);
+
+    $activeCourses = CourseManager::countActiveCourses($urlId);
+    $totalCourses = CourseManager::count_courses($urlId);
+
+    $total = Tracking::getTotalTimeSpentOnThePlatform();
+
+    $now = api_get_utc_datetime();
+
+    $beforeDateStart = new DateTime('-90 days', new DateTimeZone('UTC'));
+    $end = $beforeDateStart->format('Y-m-d H:i:s');
+
+    $thisTrimester = Tracking::getTotalTimeSpentOnThePlatform($end, $now);
+
+    $beforeDateEnd = new DateTime('-180 days', new DateTimeZone('UTC'));
+    $start = $beforeDateEnd->format('Y-m-d H:i:s');
+
+    $lastTrimester = Tracking::getTotalTimeSpentOnThePlatform($start, $end);
+
+    //var_dump($countUsers, $connectedUsers, $activeCourses, $totalCourses, $total, $thisTrimester, $lastTrimester);
+
+    $courses = Statistics::getCoursesWithActivity($startDate, $endDate);
+
+    $totalUsers = 0;
+    $totalCertificates = 0;
+    foreach ($courses as $courseId) {
+        $courseInfo = api_get_course_info_by_id($courseId);
+
+        $countUsers = CourseManager::get_user_list_from_course_code(
+            $courseInfo['code'],
+            0,
+            null,
+            null,
+            null,
+            true
+        );
+
+        $totalUsers += $countUsers;
+
+        $categories = Category::load(
+            null,
+            null,
+            $courseInfo['code'],
+            null,
+            false,
+            0
+        );
+
+        $category = null;
+        $certificateCount = 0;
+        if (!empty($categories)) {
+            $category = current($categories);
+            // @todo use count
+            $certificateCount = count(GradebookUtils::get_list_users_certificates($categoryId));
+            $totalCertificates += $certificateCount;
+        }
+    }
+
+    $totalUsersCourses = CourseManager::totalSubscribedUsersInCourses($urlId);
+}
+
+Display::display_footer();

+ 457 - 0
main/auth/sort_my_courses.php

@@ -0,0 +1,457 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$cidReset = true; // Flag forcing the 'current course' reset
+
+require_once __DIR__.'/../inc/global.inc.php';
+
+api_block_anonymous_users();
+
+$auth = new Auth();
+$user_course_categories = CourseManager::get_user_course_categories(api_get_user_id());
+$courses_in_category = $auth->get_courses_in_category();
+
+$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
+$currentUrl = api_get_self();
+
+$interbreadcrumb[] = [
+    'url' => api_get_self(),
+    'name' => get_lang('SortMyCourses'),
+];
+
+// We are moving the course of the user to a different user defined course category (=Sort My Courses).
+if (isset($_POST['submit_change_course_category'])) {
+    $result = $auth->updateCourseCategory($_POST['course_2_edit_category'], $_POST['course_categories']);
+    if ($result) {
+        Display::addFlash(
+            Display::return_message(get_lang('EditCourseCategorySucces'))
+        );
+    }
+    header('Location: '.api_get_self());
+    exit;
+}
+
+// We edit course category
+if (isset($_POST['submit_edit_course_category']) &&
+    isset($_POST['title_course_category'])
+) {
+    $result = $auth->store_edit_course_category($_POST['title_course_category'], $_POST['category_id']);
+    if ($result) {
+        Display::addFlash(
+            Display::return_message(get_lang('CourseCategoryEditStored'))
+        );
+    }
+
+    header('Location: '.api_get_self());
+    exit;
+}
+
+// We are creating a new user defined course category (= Create Course Category).
+if (isset($_POST['create_course_category']) &&
+    isset($_POST['title_course_category']) &&
+    strlen(trim($_POST['title_course_category'])) > 0
+) {
+    $result = $auth->store_course_category($_POST['title_course_category']);
+    if ($result) {
+        Display::addFlash(
+            Display::return_message(get_lang('CourseCategoryStored'))
+        );
+    } else {
+        Display::addFlash(
+            Display::return_message(
+                get_lang('ACourseCategoryWithThisNameAlreadyExists'),
+                'error'
+            )
+        );
+    }
+    header('Location: '.api_get_self());
+    exit;
+}
+
+// We are moving a course or category of the user up/down the list (=Sort My Courses).
+if (isset($_GET['move'])) {
+    if (isset($_GET['course'])) {
+        $result = $auth->move_course($_GET['move'], $_GET['course'], $_GET['category']);
+        if ($result) {
+            Display::addFlash(
+                Display::return_message(get_lang('CourseSortingDone'))
+            );
+        }
+    }
+    if (isset($_GET['category']) && !isset($_GET['course'])) {
+        $result = $auth->move_category($_GET['move'], $_GET['category']);
+        if ($result) {
+            Display::addFlash(
+                Display::return_message(get_lang('CategorySortingDone'))
+            );
+        }
+    }
+    header('Location: '.api_get_self());
+    exit;
+}
+
+switch ($action) {
+    case 'edit_category':
+        $categoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0;
+        $categoryInfo = $auth->getUserCourseCategory($categoryId);
+        if ($categoryInfo) {
+            $categoryName = $categoryInfo['title'];
+            $form = new FormValidator(
+                'edit_course_category',
+                'post',
+                $currentUrl.'?action=edit_category'
+            );
+            $form->addText('title_course_category', get_lang('Name'));
+            $form->addHidden('category_id', $categoryId);
+            $form->addButtonSave(get_lang('Edit'), 'submit_edit_course_category');
+            $form->setDefaults(['title_course_category' => $categoryName]);
+            $form->display();
+        }
+        exit;
+        break;
+    case 'edit_course_category':
+        $edit_course = (int) $_GET['course_id'];
+        $defaultCategoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0;
+        $courseInfo = api_get_course_info_by_id($edit_course);
+
+        if (empty($courseInfo)) {
+            exit;
+        }
+
+        $form = new FormValidator(
+            'edit_course_category',
+            'post',
+            $currentUrl.'?action=edit_course_category'
+        );
+
+        $form->addHeader($courseInfo['title']);
+
+        $options = [];
+        foreach ($user_course_categories as $row) {
+            $options[$row['id']] = $row['title'];
+        }
+        asort($options);
+
+        $form->addSelect(
+            'course_categories',
+            get_lang('Categories'),
+            $options,
+            ['disable_js' => true, 'placeholder' => get_lang('SelectAnOption')]
+        );
+        $form->addHidden('course_2_edit_category', $edit_course);
+
+        if (!empty($defaultCategoryId)) {
+            $form->setDefaults(['course_categories' => $defaultCategoryId]);
+        }
+        $form->addButtonSave(get_lang('Save'), 'submit_change_course_category');
+        $form->display();
+        exit;
+        break;
+    case 'deletecoursecategory':
+        // we are deleting a course category
+        if (isset($_GET['id'])) {
+            if (Security::check_token('get')) {
+                $result = $auth->delete_course_category($_GET['id']);
+                if ($result) {
+                    Display::addFlash(
+                        Display::return_message(get_lang('CourseCategoryDeleted'))
+                    );
+                }
+            }
+        }
+        header('Location: '.api_get_self());
+        exit;
+        break;
+    case 'createcoursecategory':
+        $form = new FormValidator(
+            'create_course_category',
+            'post',
+            $currentUrl.'?action=createcoursecategory'
+        );
+        $form->addText('title_course_category', get_lang('Name'));
+        $form->addButtonSave(get_lang('AddCategory'), 'create_course_category');
+        $form->display();
+        exit;
+        break;
+    case 'set_collapsable':
+        if (!api_get_configuration_value('allow_user_course_category_collapsable')) {
+            api_not_allowed(true);
+        }
+
+        $userId = api_get_user_id();
+        $categoryId = isset($_REQUEST['categoryid']) ? (int) $_REQUEST['categoryid'] : 0;
+        $option = isset($_REQUEST['option']) ? (int) $_REQUEST['option'] : 0;
+        $redirect = isset($_REQUEST['redirect']) ? $_REQUEST['redirect'] : 0;
+
+        if (empty($userId) || empty($categoryId)) {
+            api_not_allowed(true);
+        }
+
+        $table = Database::get_main_table(TABLE_USER_COURSE_CATEGORY);
+        $sql = "UPDATE $table 
+                SET collapsed = $option
+                WHERE user_id = $userId AND id = $categoryId";
+        Database::query($sql);
+        Display::addFlash(Display::return_message(get_lang('Updated')));
+
+        if ($redirect === 'home') {
+            $url = api_get_path(WEB_PATH).'user_portal.php';
+            header('Location: '.$url);
+            exit;
+        }
+
+        $url = api_get_self();
+        header('Location: '.$url);
+        exit;
+        break;
+}
+
+Display::display_header();
+
+$stok = Security::get_token();
+$courses_without_category = isset($courses_in_category[0]) ? $courses_in_category[0] : null;
+echo '<div id="actions" class="actions">';
+if ($action != 'createcoursecategory') {
+    echo '<a class="ajax" href="'.$currentUrl.'?action=createcoursecategory">';
+    echo Display::return_icon('new_folder.png', get_lang('CreateCourseCategory'), '', '32');
+    echo '</a>';
+}
+echo '</div>';
+
+if (!empty($message)) {
+    echo Display::return_message($message, 'confirm', false);
+}
+
+$allowCollapsable = api_get_configuration_value('allow_user_course_category_collapsable');
+$teachersIcon = Display::return_icon('teacher.png', get_lang('Teachers'), null, ICON_SIZE_TINY);
+
+// COURSES WITH CATEGORIES
+if (!empty($user_course_categories)) {
+    $counter = 0;
+    $last = end($user_course_categories);
+    foreach ($user_course_categories as $row) {
+        echo Display::page_subheader($row['title']);
+        echo '<a name="category'.$row['id'].'"></a>';
+        $url = $currentUrl.'?categoryid='.$row['id'].'&sec_token='.$stok;
+        if ($allowCollapsable) {
+            if (isset($row['collapsed']) && $row['collapsed'] == 0) {
+                echo Display::url(
+                    '<i class="fa fa-folder-open"></i>',
+                    $url.'&action=set_collapsable&option=1'
+                );
+            } else {
+                echo Display::url(
+                    '<i class="fa fa-folder"></i>',
+                    $url.'&action=set_collapsable&option=0'
+                );
+            }
+        }
+
+        echo Display::url(
+            Display::return_icon('edit.png', get_lang('Edit'), '', 22),
+            $currentUrl.'?action=edit_category&category_id='.$row['id'].'&sec_token='.$stok,
+            ['class' => 'ajax']
+        );
+
+        if (0 != $counter) {
+            echo Display::url(
+                Display::return_icon('up.png', get_lang('Up'), '', 22),
+                $currentUrl.'?move=up&category='.$row['id'].'&sec_token='.$stok
+            );
+        } else {
+            echo Display::return_icon('up_na.png', get_lang('Up'), '', 22);
+        }
+        if ($row['id'] != $last['id']) {
+            echo Display::url(
+                Display::return_icon('down.png', get_lang('Down'), '', 22),
+                $currentUrl.'?move=down&category='.$row['id'].'&sec_token='.$stok
+            );
+        } else {
+            echo Display::return_icon('down_na.png', get_lang('Down'), '', 22);
+        }
+
+        echo Display::url(
+            Display::return_icon(
+                'delete.png',
+                get_lang('Delete'),
+                [
+                    'onclick' => "javascript: if (!confirm('".addslashes(
+                            api_htmlentities(
+                                get_lang('CourseCategoryAbout2bedeleted'),
+                                ENT_QUOTES,
+                                api_get_system_encoding()
+                            )
+                        )."')) return false;",
+                ],
+                22
+            ),
+            $currentUrl.'?action=deletecoursecategory&id='.$row['id'].'&sec_token='.$stok
+        );
+
+        $counter++;
+        echo '<br /><br />';
+        // Show the courses inside this category
+        echo '<table class="data_table">';
+        $number_of_courses = isset($courses_in_category[$row['id']]) ? count($courses_in_category[$row['id']]) : 0;
+        $key = 0;
+        if (!empty($courses_in_category[$row['id']])) {
+            foreach ($courses_in_category[$row['id']] as $course) {
+                echo '<tr><td>';
+                echo '<a name="course'.$course['code'].'"></a>';
+                echo '<strong>'.$course['title'].'</strong>';
+                echo ' ('.$course['visual_code'].')';
+                echo '<br />';
+                echo $teachersIcon;
+                echo '&nbsp;';
+                echo CourseManager::getTeacherListFromCourseCodeToString($course['code']);
+                echo '<br />';
+
+                if (api_get_setting('display_teacher_in_courselist') === 'true') {
+                    echo $course['tutor'];
+                }
+                echo '</td><td valign="top">'; ?>
+                <div style="float:left;width:110px;">
+                <?php
+                    if (api_get_setting('show_courses_descriptions_in_catalog') == 'true') {
+                        $icon_title = get_lang('CourseDetails').' - '.$course['title']; ?>
+                <a href="<?php echo api_get_path(WEB_CODE_PATH); ?>inc/ajax/course_home.ajax.php?a=show_course_information&code=<?php echo $course['code']; ?>" data-title="<?php echo $icon_title; ?>" title="<?php echo $icon_title; ?>" class="ajax">
+                    <?php
+                    echo Display::return_icon('info.png', $icon_title, '', '22');
+                    } ?>
+                </a>
+                <?php
+                    echo Display::url(
+                        Display::return_icon('edit.png', get_lang('Edit'), '', 22),
+                        $currentUrl.'?action=edit_course_category&category_id='.$row['id'].'&course_id='.$course['real_id'].'&sec_token='.$stok,
+                        ['class' => 'ajax']
+                    );
+
+                if ($key > 0) {
+                    ?>
+                    <a href="<?php echo $currentUrl; ?>?action=<?php echo $action; ?>&amp;move=up&amp;course=<?php echo $course['code']; ?>&amp;category=<?php echo $course['user_course_cat']; ?>&amp;sec_token=<?php echo $stok; ?>">
+                    <?php echo Display::display_icon('up.png', get_lang('Up'), '', 22); ?>
+                    </a>
+                <?php
+                } else {
+                    echo Display::display_icon('up_na.png', get_lang('Up'), '', 22);
+                }
+                if ($key < $number_of_courses - 1) {
+                    ?>
+                    <a href="<?php echo $currentUrl; ?>?action=<?php echo $action; ?>&amp;move=down&amp;course=<?php echo $course['code']; ?>&amp;category=<?php echo $course['user_course_cat']; ?>&amp;sec_token=<?php echo $stok; ?>">
+                    <?php echo Display::return_icon('down.png', get_lang('Down'), '', 22); ?>
+                    </a>
+                <?php
+                } else {
+                    echo Display::return_icon('down_na.png', get_lang('Down'), '', 22);
+                } ?>
+              </div>
+              <div style="float:left; margin-right:10px;">
+                <?php
+                    if ($course['status'] != 1) {
+                        if ($course['unsubscr'] == 1) {
+                            ?>
+
+                <form action="<?php echo api_get_self(); ?>" method="post" onsubmit="javascript: if (!confirm('<?php echo addslashes(api_htmlentities(get_lang("ConfirmUnsubscribeFromCourse"), ENT_QUOTES, api_get_system_encoding())); ?>')) return false">
+                    <input type="hidden" name="sec_token" value="<?php echo $stok; ?>">
+                    <input type="hidden" name="unsubscribe" value="<?php echo $course['code']; ?>" />
+                     <button class="btn btn-default" value="<?php echo get_lang('Unsubscribe'); ?>" name="unsub">
+                    <?php echo get_lang('Unsubscribe'); ?>
+                    </button>
+                </form>
+              </div>
+                  <?php
+                        }
+                    }
+                $key++;
+            }
+            echo '</table>';
+        }
+    }
+}
+
+echo Display::page_subheader(get_lang('NoCourseCategory'));
+echo '<table class="data_table">';
+// COURSES WITHOUT CATEGORY
+if (!empty($courses_without_category)) {
+    $number_of_courses = count($courses_without_category);
+    $key = 0;
+    foreach ($courses_without_category as $course) {
+        echo '<tr><td>';
+        echo '<a name="course'.$course['code'].'"></a>';
+        echo '<strong>'.$course['title'].'</strong>';
+        echo ' ('.$course['visual_code'].')';
+        echo '<br />';
+        echo $teachersIcon;
+        echo '&nbsp;';
+        echo CourseManager::getTeacherListFromCourseCodeToString($course['code']);
+        echo '<br />';
+
+        if (api_get_setting('display_teacher_in_courselist') === 'true') {
+            echo $course['tutor'];
+        }
+        echo '</td><td valign="top">'; ?>
+            <div style="float:left; width:110px">
+            <?php
+            if (api_get_setting('show_courses_descriptions_in_catalog') == 'true') {
+                $icon_title = get_lang('CourseDetails').' - '.$course['title']; ?>
+            <a href="<?php echo api_get_path(WEB_CODE_PATH); ?>inc/ajax/course_home.ajax.php?a=show_course_information&code=<?php echo $course['code']; ?>" data-title="<?php echo $icon_title; ?>" title="<?php echo $icon_title; ?>" class="ajax">
+                <?php echo Display::return_icon('info.png', $icon_title, '', '22'); ?>
+            </a>
+            <?php
+            }
+        echo '';
+        if (isset($_GET['edit']) && $course['code'] == $_GET['edit']) {
+            echo Display::return_icon('edit_na.png', get_lang('Edit'), '', 22);
+        } else {
+            echo Display::url(
+                Display::return_icon('edit.png', get_lang('Edit'), '', 22),
+                $currentUrl.'?action=edit_course_category&course_id='.$course['real_id'].'&'.$stok,
+                ['class' => 'ajax']
+            );
+        }
+        if ($key > 0) {
+            ?>
+                <a href="<?php echo $currentUrl; ?>?action=<?php echo $action; ?>&amp;move=up&amp;course=<?php echo $course['code']; ?>&amp;category=<?php echo $course['user_course_cat']; ?>&amp;sec_token=<?php echo $stok; ?>">
+                <?php echo Display::display_icon('up.png', get_lang('Up'), '', 22); ?>
+                </a>
+            <?php
+        } else {
+            echo Display::return_icon('up_na.png', get_lang('Up'), '', 22);
+        }
+        if ($key < $number_of_courses - 1) {
+            ?>
+                <a href="<?php echo $currentUrl; ?>?action=<?php echo $action; ?>&amp;move=down&amp;course=<?php echo $course['code']; ?>&amp;category=<?php echo $course['user_course_cat']; ?>&amp;sec_token=<?php echo $stok; ?>">
+                <?php echo Display::display_icon('down.png', get_lang('Down'), '', 22); ?>
+                </a>
+            <?php
+        } else {
+            echo Display::return_icon('down_na.png', get_lang('Down'), '', 22);
+        } ?>
+                </div>
+                 <div style="float:left; margin-right:10px;">
+            <?php
+                if ($course['status'] != 1) {
+                    if ($course['unsubscr'] == 1) {
+                        ?>
+                <!-- changed link to submit to avoid action by the search tool indexer -->
+                <form action="<?php echo api_get_self(); ?>" method="post" onsubmit="javascript: if (!confirm('<?php echo addslashes(api_htmlentities(get_lang("ConfirmUnsubscribeFromCourse"), ENT_QUOTES, api_get_system_encoding())); ?>')) return false;">
+                    <input type="hidden" name="sec_token" value="<?php echo $stok; ?>">
+                    <input type="hidden" name="unsubscribe" value="<?php echo $course['code']; ?>" />
+                    <button class="btn btn-default" value="<?php echo get_lang('Unsubscribe'); ?>" name="unsub">
+                        <?php echo get_lang('Unsubscribe'); ?>
+                    </button>
+                </form>
+                </div>
+              <?php
+                    }
+                } ?>
+            </td>
+            </tr>
+            <?php
+            $key++;
+    }
+}
+?>
+</table>
+<?php
+Display::display_footer();

+ 36 - 0
main/inc/lib/hook/HookConditionalLogin.php

@@ -0,0 +1,36 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Class HookConditionalLogin.
+ * Hook to implement Conditional Login.
+ */
+class HookConditionalLogin extends HookEvent implements HookConditionalLoginEventInterface
+{
+    /**
+     * HookConditionalLogin constructor.
+     *
+     * @throws Exception
+     */
+    protected function __construct()
+    {
+        parent::__construct('HookConditionalLogin');
+    }
+
+    /**
+     * Notify to all hook observers.
+     *
+     * @return array
+     */
+    public function notifyConditionalLogin()
+    {
+        $conditions = [];
+
+        /** @var HookConditionalLoginObserverInterface $observer */
+        foreach ($this->observers as $observer) {
+            $conditions[] = $observer->hookConditionalLogin($this);
+        }
+
+        return $conditions;
+    }
+}

+ 54 - 0
main/inc/lib/hook/HookMyStudentsLpTracking.php

@@ -0,0 +1,54 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Class HookMyStudentsLpTracking.
+ */
+class HookMyStudentsLpTracking extends HookEvent implements HookMyStudentsLpTrackingEventInterface
+{
+    /**
+     * HookMyStudentsLpTracking constructor.
+     *
+     * @throws Exception
+     */
+    protected function __construct()
+    {
+        parent::__construct('HookMyStudentsLpTracking');
+    }
+
+    /**
+     * @return array
+     */
+    public function notifyTrackingHeader()
+    {
+        $results = [];
+
+        /** @var HookMyStudentsLpTrackingObserverInterface $observer */
+        foreach ($this->observers as $observer) {
+            $results[] = $observer->trackingHeader($this);
+        }
+
+        return $results;
+    }
+
+    /**
+     * @param int $lpId
+     * @param int $studentId
+     *
+     * @return array
+     */
+    public function notifyTrackingContent($lpId, $studentId)
+    {
+        $this->eventData['lp_id'] = $lpId;
+        $this->eventData['student_id'] = $studentId;
+
+        $results = [];
+
+        /** @var HookMyStudentsLpTrackingObserverInterface $observer */
+        foreach ($this->observers as $observer) {
+            $results[] = $observer->trackingContent($this);
+        }
+
+        return $results;
+    }
+}

+ 54 - 0
main/inc/lib/hook/HookMyStudentsQuizTracking.php

@@ -0,0 +1,54 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Class HookMyStudentsQuizTracking.
+ */
+class HookMyStudentsQuizTracking extends HookEvent implements HookMyStudentsQuizTrackingEventInterface
+{
+    /**
+     * HookMyStudentsQuizTracking constructor.
+     *
+     * @throws Exception
+     */
+    protected function __construct()
+    {
+        parent::__construct('HookMyStudentsQuizTracking');
+    }
+
+    /**
+     * @return array
+     */
+    public function notifyTrackingHeader()
+    {
+        $results = [];
+
+        /** @var HookMyStudentsQuizTrackingObserverInterface $observer */
+        foreach ($this->observers as $observer) {
+            $results[] = $observer->trackingHeader($this);
+        }
+
+        return $results;
+    }
+
+    /**
+     * @param int $quizId
+     * @param int $studentId
+     *
+     * @return array
+     */
+    public function notifyTrackingContent($quizId, $studentId)
+    {
+        $this->eventData['quiz_id'] = $quizId;
+        $this->eventData['student_id'] = $studentId;
+
+        $results = [];
+
+        /** @var HookMyStudentsQuizTrackingObserverInterface $observer */
+        foreach ($this->observers as $observer) {
+            $results[] = $observer->trackingContent($this);
+        }
+
+        return $results;
+    }
+}

+ 15 - 0
main/inc/lib/hook/interfaces/HookConditionalLoginEventInterface.php

@@ -0,0 +1,15 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookConditionalLoginEventInterface.
+ */
+interface HookConditionalLoginEventInterface extends HookEventInterface
+{
+    /**
+     * Call Conditional Login hooks.
+     *
+     * @return array
+     */
+    public function notifyConditionalLogin();
+}

+ 24 - 0
main/inc/lib/hook/interfaces/HookConditionalLoginObserverInterface.php

@@ -0,0 +1,24 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookConditionalLoginObserverInterface.
+ */
+interface HookConditionalLoginObserverInterface extends HookObserverInterface
+{
+    /**
+     * Return an associative array (callable, url) needed for Conditional Login.
+     * <code>
+     * [
+     *     'conditional_function' => function (array $userInfo) {},
+     *     'url' => '',
+     * ]
+     * </code>
+     * conditional_function returns false to redirect to the url and returns true to continue with the classical login.
+     *
+     * @param HookConditionalLoginEventInterface $hook
+     *
+     * @return array
+     */
+    public function hookConditionalLogin(HookConditionalLoginEventInterface $hook);
+}

+ 21 - 0
main/inc/lib/hook/interfaces/HookMyStudentsLpTrackingEventInterface.php

@@ -0,0 +1,21 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookMyStudentsLpTrackingEventInterface.
+ */
+interface HookMyStudentsLpTrackingEventInterface extends HookEventInterface
+{
+    /**
+     * @return array
+     */
+    public function notifyTrackingHeader();
+
+    /**
+     * @param int $lpId
+     * @param int $studentId
+     *
+     * @return array
+     */
+    public function notifyTrackingContent($lpId, $studentId);
+}

+ 38 - 0
main/inc/lib/hook/interfaces/HookMyStudentsLpTrackingObserverInterface.php

@@ -0,0 +1,38 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookMyStudentsLpTrackingObserverInterface.
+ */
+interface HookMyStudentsLpTrackingObserverInterface extends HookObserverInterface
+{
+    /**
+     * Return an associative array this value and attributes.
+     * <code>
+     * [
+     *     'value' => 'Users online',
+     *     'attrs' => ['class' => 'text-center'],
+     * ]
+     * </code>.
+     *
+     * @param HookMyStudentsLpTrackingEventInterface $hook
+     *
+     * @return array
+     */
+    public function trackingHeader(HookMyStudentsLpTrackingEventInterface $hook);
+
+    /**
+     * Return an associative array this value and attributes.
+     * <code>
+     * [
+     *     'value' => '5 connected users ',
+     *     'attrs' => ['class' => 'text-center text-success'],
+     * ]
+     * </code>.
+     *
+     * @param HookMyStudentsLpTrackingEventInterface $hook
+     *
+     * @return array
+     */
+    public function trackingContent(HookMyStudentsLpTrackingEventInterface $hook);
+}

+ 21 - 0
main/inc/lib/hook/interfaces/HookMyStudentsQuizTrackingEventInterface.php

@@ -0,0 +1,21 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookMyStudentsQuizTrackingEventInterface.
+ */
+interface HookMyStudentsQuizTrackingEventInterface extends HookEventInterface
+{
+    /**
+     * @return array
+     */
+    public function notifyTrackingHeader();
+
+    /**
+     * @param int $quizId
+     * @param int $studentId
+     *
+     * @return array
+     */
+    public function notifyTrackingContent($quizId, $studentId);
+}

+ 38 - 0
main/inc/lib/hook/interfaces/HookMyStudentsQuizTrackingObserverInterface.php

@@ -0,0 +1,38 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Interface HookMyStudentsQuizTrackingObserverInterface.
+ */
+interface HookMyStudentsQuizTrackingObserverInterface extends HookObserverInterface
+{
+    /**
+     * Return an associative array this value and attributes.
+     * <code>
+     * [
+     *     'value' => 'Users online',
+     *     'attrs' => ['class' => 'text-center'],
+     * ]
+     * </code>.
+     *
+     * @param HookMyStudentsQuizTrackingEventInterface $hook
+     *
+     * @return array
+     */
+    public function trackingHeader(HookMyStudentsQuizTrackingEventInterface $hook);
+
+    /**
+     * Return an associative array this value and attributes.
+     * <code>
+     * [
+     *     'value' => '5 connected users ',
+     *     'attrs' => ['class' => 'text-center text-success'],
+     * ]
+     * </code>.
+     *
+     * @param HookMyStudentsQuizTrackingEventInterface $hook
+     *
+     * @return array
+     */
+    public function trackingContent(HookMyStudentsQuizTrackingEventInterface $hook);
+}

+ 99 - 0
main/user/career_diagram.php

@@ -0,0 +1,99 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/*
+ *
+ * Requires extra_field_values.value to be longtext to save diagram:
+ *
+UPDATE extra_field_values SET created_at = NULL WHERE CAST(created_at AS CHAR(20)) = '0000-00-00 00:00:00';
+UPDATE extra_field_values SET updated_at = NULL WHERE CAST(updated_at AS CHAR(20)) = '0000-00-00 00:00:00';
+ALTER TABLE extra_field_values modify column value longtext null;
+*/
+
+require_once __DIR__.'/../inc/global.inc.php';
+
+if (api_get_configuration_value('allow_career_diagram') == false) {
+    api_not_allowed(true);
+}
+
+$this_section = SECTION_COURSES;
+
+$careerId = isset($_GET['career_id']) ? $_GET['career_id'] : 0;
+
+if (empty($careerId)) {
+    api_not_allowed(true);
+}
+
+$career = new Career();
+$careerInfo = $career->get($careerId);
+if (empty($careerInfo)) {
+    api_not_allowed(true);
+}
+
+$userId = api_get_user_id();
+$allow = UserManager::userHasCareer($userId, $careerId) || api_is_platform_admin();
+
+if ($allow === false) {
+    api_not_allowed(true);
+}
+
+$htmlHeadXtra[] = api_get_js('jsplumb2.js');
+$htmlHeadXtra[] = api_get_asset('qtip2/jquery.qtip.min.js');
+$htmlHeadXtra[] = api_get_css_asset('qtip2/jquery.qtip.min.css');
+
+// setting breadcrumbs
+$interbreadcrumb[] = [
+    'url' => api_get_path(WEB_CODE_PATH).'auth/my_progress.php',
+    'name' => get_lang('Progress'),
+];
+
+$interbreadcrumb[] = [
+    'url' => '#',
+    'name' => get_lang('Careers'),
+];
+
+$extraFieldValue = new ExtraFieldValue('career');
+
+// Check urls
+$itemUrls = $extraFieldValue->get_values_by_handler_and_field_variable(
+    $careerId,
+    'career_urls',
+    false,
+    false,
+    false
+);
+
+$urlToString = '';
+if (!empty($itemUrls) && !empty($itemUrls['value'])) {
+    $urls = explode(',', $itemUrls['value']);
+    $urlToString = '&nbsp;&nbsp;';
+    if (!empty($urls)) {
+        foreach ($urls as $urlData) {
+            $urlData = explode('@', $urlData);
+            if (isset($urlData[1])) {
+                $urlToString .= Display::url($urlData[0], $urlData[1]).'&nbsp;';
+            } else {
+                $urlToString .= $urlData[0].'&nbsp;';
+            }
+        }
+    }
+}
+
+$tpl = new Template(get_lang('Diagram'));
+$html = Display::page_subheader2($careerInfo['name'].$urlToString);
+$diagram = Career::renderDiagramByColumn($careerInfo, $tpl, $userId);
+
+if (!empty($diagram)) {
+    $html .= $diagram;
+} else {
+    Display::addFlash(
+        Display::return_message(
+            sprintf(get_lang('CareerXDoesntHaveADiagram'), $careerInfo['name']),
+            'warning'
+        )
+    );
+}
+
+$tpl->assign('content', $html);
+$layout = $tpl->get_template('career/diagram.tpl');
+$tpl->display($layout);