From 107a61c9cbdb59b64df4561ffcb9ce200175280f Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 17 Jan 2025 01:11:26 -0500 Subject: [PATCH 01/33] Unify router modules for course and session catalog --- assets/vue/router/catalogue.js | 17 +++++++++++++++++ assets/vue/router/cataloguecourses.js | 6 ------ assets/vue/router/cataloguesessions.js | 6 ------ assets/vue/router/index.js | 6 ++---- 4 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 assets/vue/router/catalogue.js delete mode 100644 assets/vue/router/cataloguecourses.js delete mode 100644 assets/vue/router/cataloguesessions.js diff --git a/assets/vue/router/catalogue.js b/assets/vue/router/catalogue.js new file mode 100644 index 0000000000..9c58ca388e --- /dev/null +++ b/assets/vue/router/catalogue.js @@ -0,0 +1,17 @@ +export default { + path: "/catalogue", + meta: { requiresAdmin: true, requiresSessionAdmin: true }, + component: () => import("../components/layout/SimpleRouterViewLayout.vue"), + children: [ + { + path: "courses", + name: "CatalogueCourses", + component: () => import("../views/course/CatalogueCourses.vue"), + }, + { + path: "sessions", + name: "CatalogueSessions", + component: () => import("../views/course/CatalogueSessions.vue"), + }, + ], +} diff --git a/assets/vue/router/cataloguecourses.js b/assets/vue/router/cataloguecourses.js deleted file mode 100644 index 29c97e03bc..0000000000 --- a/assets/vue/router/cataloguecourses.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - path: "/catalogue/courses", - name: "CatalogueCourses", - meta: { requiresAdmin: true, requiresSessionAdmin: true }, - component: () => import("../views/course/CatalogueCourses.vue"), -} diff --git a/assets/vue/router/cataloguesessions.js b/assets/vue/router/cataloguesessions.js deleted file mode 100644 index 336307b201..0000000000 --- a/assets/vue/router/cataloguesessions.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - path: "/catalogue/sessions", - name: "CatalogueSessions", - meta: { requiresAdmin: true, requiresSessionAdmin: true }, - component: () => import("../views/course/CatalogueSessions.vue"), -} diff --git a/assets/vue/router/index.js b/assets/vue/router/index.js index 86e51c5cae..e772920745 100644 --- a/assets/vue/router/index.js +++ b/assets/vue/router/index.js @@ -21,6 +21,7 @@ import documents from "./documents" import assignments from "./assignments" import links from "./links" import glossary from "./glossary" +import catalogue from "./catalogue" import { useSecurityStore } from "../store/securityStore" import MyCourseList from "../views/user/courses/List.vue" import MySessionList from "../views/user/sessions/SessionsCurrent.vue" @@ -41,8 +42,6 @@ import Demo from "../pages/Demo.vue" import { useCidReqStore } from "../store/cidReq" import courseService from "../services/courseService" -import catalogueCourses from "./cataloguecourses" -import catalogueSessions from "./cataloguesessions" import { customVueTemplateEnabled } from "../config/env" import { useCourseSettings } from "../store/courseSettingStore" import { checkIsAllowedToEdit, useUserSessionSubscription } from "../composables/userPermissions" @@ -227,8 +226,7 @@ const router = createRouter({ fileManagerRoutes, termsRoutes, socialNetworkRoutes, - catalogueCourses, - catalogueSessions, + catalogue, adminRoutes, courseRoutes, //courseCategoryRoutes, From 304ea7d790f3b69dc457791bea6b73cb6e1bc3d0 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:39:32 -0500 Subject: [PATCH 02/33] Display: Fix double anchor tag for custom router-link --- assets/css/app.scss | 4 ++++ assets/css/scss/molecules/_course_tool.scss | 5 ++++- .../components/basecomponents/BaseAppLink.vue | 16 ++-------------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/assets/css/app.scss b/assets/css/app.scss index 10c4664734..03c0a8e098 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -292,6 +292,10 @@ } } +.mdi { + @apply font-normal text-base leading-none; +} + .field > small.p-error { @apply text-error; } diff --git a/assets/css/scss/molecules/_course_tool.scss b/assets/css/scss/molecules/_course_tool.scss index 2c6f48d7ca..0a985c8d29 100644 --- a/assets/css/scss/molecules/_course_tool.scss +++ b/assets/css/scss/molecules/_course_tool.scss @@ -13,7 +13,10 @@ &__icon { @apply text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-gradient leading-none; - font-size: 52px; + &, + &.mdi { + font-size: 52px; + } &::before { } diff --git a/assets/vue/components/basecomponents/BaseAppLink.vue b/assets/vue/components/basecomponents/BaseAppLink.vue index cbcdad67b8..641675da03 100644 --- a/assets/vue/components/basecomponents/BaseAppLink.vue +++ b/assets/vue/components/basecomponents/BaseAppLink.vue @@ -2,10 +2,6 @@ import { RouterLink } from "vue-router" import { computed } from "vue" -defineOptions({ - inheritAttrs: false, -}) - const props = defineProps({ ...RouterLink.props, url: { @@ -28,16 +24,8 @@ const isAnchor = computed(() => !!props.url) - - - + From fc4bda91c4db6b1a32d0def490964e1fbd3ac38d Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:52:38 -0500 Subject: [PATCH 03/33] Display: Register BaseAppLink as global component --- assets/vue/components/Breadcrumb.vue | 1 - assets/vue/components/admin/AdminBlock.vue | 1 - assets/vue/components/basecomponents/BaseRouteTabs.vue | 3 --- assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue | 1 - assets/vue/components/course/CourseCard.vue | 1 - assets/vue/components/course/CourseTool.vue | 1 - assets/vue/components/course/ShortCutList.vue | 1 - assets/vue/components/documents/DocumentEntry.vue | 1 - assets/vue/components/layout/PlatformLogo.vue | 1 - assets/vue/components/layout/TopbarLoggedIn.vue | 2 -- assets/vue/components/login/LoginExternalButtons.vue | 1 - assets/vue/components/resource_links/ShowLinks.vue | 1 - assets/vue/components/social/SocialGroupMenu.vue | 1 - assets/vue/components/social/SocialSideMenu.vue | 1 - assets/vue/components/social/SocialWallPost.vue | 1 - assets/vue/main.js | 2 ++ assets/vue/views/message/MessageList.vue | 1 - 17 files changed, 2 insertions(+), 19 deletions(-) diff --git a/assets/vue/components/Breadcrumb.vue b/assets/vue/components/Breadcrumb.vue index 5da95c0daf..b1311d1da1 100644 --- a/assets/vue/components/Breadcrumb.vue +++ b/assets/vue/components/Breadcrumb.vue @@ -31,7 +31,6 @@ import { useI18n } from "vue-i18n" import Breadcrumb from "primevue/breadcrumb" import { useCidReqStore } from "../store/cidReq" import { storeToRefs } from "pinia" -import BaseAppLink from "./basecomponents/BaseAppLink.vue" const legacyItems = ref(window.breadcrumb) diff --git a/assets/vue/components/admin/AdminBlock.vue b/assets/vue/components/admin/AdminBlock.vue index 7894965af2..8e5bba5f31 100644 --- a/assets/vue/components/admin/AdminBlock.vue +++ b/assets/vue/components/admin/AdminBlock.vue @@ -76,7 +76,6 @@ import { useI18n } from "vue-i18n" import BaseInputGroup from "../basecomponents/BaseInputGroup.vue" import BaseIcon from "../basecomponents/BaseIcon.vue" import AdminBlockExtraContent from "./AdminBlockExtraContent.vue" -import BaseAppLink from "../basecomponents/BaseAppLink.vue" const { t } = useI18n() diff --git a/assets/vue/components/basecomponents/BaseRouteTabs.vue b/assets/vue/components/basecomponents/BaseRouteTabs.vue index 5a1ef797e0..9d313582f8 100644 --- a/assets/vue/components/basecomponents/BaseRouteTabs.vue +++ b/assets/vue/components/basecomponents/BaseRouteTabs.vue @@ -23,9 +23,6 @@ * Component that will render a tab interface WITHOUT content. Every tab should be a router link. So, when user * change tab the route of the url will change */ - -import BaseAppLink from "./BaseAppLink.vue" - defineProps({ tabs: { type: Array, diff --git a/assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue b/assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue index eb6ec96244..bde4177988 100644 --- a/assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue +++ b/assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue @@ -1,5 +1,4 @@ + + From 68c26ef10f466b74b2be2ffefa6fe96a9d395be3 Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Mon, 20 Jan 2025 16:31:54 +0100 Subject: [PATCH 05/33] Minor: Remove debug flag from exercise.ajax.php --- public/main/inc/ajax/exercise.ajax.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/main/inc/ajax/exercise.ajax.php b/public/main/inc/ajax/exercise.ajax.php index 39781fa0b6..036caf8fe6 100644 --- a/public/main/inc/ajax/exercise.ajax.php +++ b/public/main/inc/ajax/exercise.ajax.php @@ -8,7 +8,7 @@ use ChamiloSession as Session; require_once __DIR__.'/../global.inc.php'; $current_course_tool = TOOL_QUIZ; -$debug = true; +$debug = false; api_protect_course_script(true); $action = $_REQUEST['a']; $course_id = api_get_course_int_id(); From 475c5636f93328ccc8369a3f28cb663d13d60cf1 Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Mon, 20 Jan 2025 18:50:06 +0100 Subject: [PATCH 06/33] Minor: Add doc on legal_accept extra field format --- public/main/auth/inscription.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/main/auth/inscription.php b/public/main/auth/inscription.php index b97a55ff63..97fff339a1 100644 --- a/public/main/auth/inscription.php +++ b/public/main/auth/inscription.php @@ -1089,6 +1089,7 @@ if ($form->validate()) { $cond_array = explode(':', $values['legal_accept_type']); if (!empty($cond_array[0]) && !empty($cond_array[1])) { $time = time(); + // legal_accept is stored as version_id:language_id:timestamp $conditionToSave = (int) $cond_array[0].':'.(int) $cond_array[1].':'.$time; UserManager::update_extra_field_value( $userId, From f93c232107417ee038155810e5cdff46bbe8ad2d Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:27:19 -0500 Subject: [PATCH 07/33] Display: Add BaseRating component --- assets/css/scss/atoms/_rating.scss | 44 +++++++++++++++++++ assets/css/scss/index.scss | 1 + .../components/basecomponents/BaseRating.vue | 26 +++++++++++ 3 files changed, 71 insertions(+) create mode 100644 assets/css/scss/atoms/_rating.scss create mode 100644 assets/vue/components/basecomponents/BaseRating.vue diff --git a/assets/css/scss/atoms/_rating.scss b/assets/css/scss/atoms/_rating.scss new file mode 100644 index 0000000000..302ca77985 --- /dev/null +++ b/assets/css/scss/atoms/_rating.scss @@ -0,0 +1,44 @@ +.p-rating { + @apply relative flex items-center gap-1; + + &-item { + @apply inline-flex items-center cursor-pointer outline-none rounded-lg + hover:outline-none hover:drop-shadow-lg; + + .p-rating-icon { + @apply transition-none text-gray-50; + + font-size: 1rem; + + &.p-icon { + @apply w-4 h-4; + + &.p-rating-cancel { + @apply text-danger; + } + } + } + + &.p-focus { + @apply outline-none drop-shadow-lg; + } + + &.p-rating-item-active { + & .p-rating-icon { + @apply text-warning; + } + } + } + + &.p-readonly &-item { + @apply cursor-default; + } + + &:not(.p-disabled):not(.p-readonly) &-item:hover &-icon { + @apply text-warning; + } + + &:not(.p-disabled):not(.p-readonly) &-item:hover &-icon.p-rating-cancel { + @apply text-danger; + } +} diff --git a/assets/css/scss/index.scss b/assets/css/scss/index.scss index a8ebf04387..f62e4dae56 100755 --- a/assets/css/scss/index.scss +++ b/assets/css/scss/index.scss @@ -47,6 +47,7 @@ @include meta.load-css("atoms/platform_logo"); @include meta.load-css("atoms/progressbar"); @include meta.load-css("atoms/radio"); +@include meta.load-css("atoms/rating"); @include meta.load-css("atoms/skeleton"); @include meta.load-css("atoms/tags"); @include meta.load-css("atoms/toast"); diff --git a/assets/vue/components/basecomponents/BaseRating.vue b/assets/vue/components/basecomponents/BaseRating.vue new file mode 100644 index 0000000000..4667a4cdf8 --- /dev/null +++ b/assets/vue/components/basecomponents/BaseRating.vue @@ -0,0 +1,26 @@ + + + From a2f3e3d9878b7a840b175331a3ac21594770cd06 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Thu, 23 Jan 2025 02:02:39 -0500 Subject: [PATCH 08/33] Admin: Add duplicate_specific_session_content_on_session_copy setting - refs BT#22254 See eea5cfdfce71334f8ca7955bac9856ebed236f73 --- public/main/inc/lib/api.lib.php | 27 ++++- public/main/inc/lib/course.lib.php | 9 +- public/main/inc/lib/promotion.lib.php | 2 +- public/main/inc/lib/sessionmanager.lib.php | 30 +++-- public/main/session/session_list.php | 52 ++++++++- .../Settings/SessionSettingsSchema.php | 2 + .../Component/CourseCopy/CourseRestorer.php | 109 +++++++++--------- 7 files changed, 156 insertions(+), 75 deletions(-) diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index fe31007fc9..46b8aeb33b 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -4082,14 +4082,16 @@ function copy_folder_course_session( $session_id, $course_info, $document, - $source_course_id + $source_course_id, + array $originalFolderNameList = [], + string $originalBaseName = '' ) { $table = Database::get_course_table(TABLE_DOCUMENT); $session_id = intval($session_id); $source_course_id = intval($source_course_id); // Check whether directory already exists. - if (is_dir($pathname) || empty($pathname)) { + if (empty($pathname) || is_dir($pathname)) { return true; } @@ -4100,12 +4102,20 @@ function copy_folder_course_session( return false; } + $baseNoDocument = str_replace('document', '', $originalBaseName); + $folderTitles = explode('/', $baseNoDocument); + $folderTitles = array_filter($folderTitles); + + $table = Database::get_course_table(TABLE_DOCUMENT); + $session_id = (int) $session_id; + $source_course_id = (int) $source_course_id; + $course_id = $course_info['real_id']; $folders = explode(DIRECTORY_SEPARATOR, str_replace($base_path_document.DIRECTORY_SEPARATOR, '', $pathname)); $new_pathname = $base_path_document; $path = ''; - foreach ($folders as $folder) { + foreach ($folders as $index => $folder) { $new_pathname .= DIRECTORY_SEPARATOR.$folder; $path .= DIRECTORY_SEPARATOR.$folder; @@ -4123,13 +4133,22 @@ function copy_folder_course_session( if (0 == $num_rows) { mkdir($new_pathname, api_get_permissions_for_new_directories()); + $title = basename($new_pathname); + + if (isset($folderTitles[$index + 1])) { + $checkPath = $folderTitles[$index +1]; + + if (isset($originalFolderNameList[$checkPath])) { + $title = $originalFolderNameList[$checkPath]; + } + } // Insert new folder with destination session_id. $params = [ 'c_id' => $course_id, 'path' => $path, 'comment' => $document->comment, - 'title' => basename($new_pathname), + 'title' => $title, 'filetype' => 'folder', 'size' => '0', 'session_id' => $session_id, diff --git a/public/main/inc/lib/course.lib.php b/public/main/inc/lib/course.lib.php index f64255fcdb..a1081d8c08 100644 --- a/public/main/inc/lib/course.lib.php +++ b/public/main/inc/lib/course.lib.php @@ -4220,7 +4220,8 @@ class CourseManager $destination_course_code, $destination_session_id, $params = [], - $withBaseContent = true + bool $withBaseContent = true, + bool $copySessionContent = false ) { $course_info = api_get_course_info($source_course_code); @@ -4228,6 +4229,7 @@ class CourseManager $cb = new CourseBuilder('', $course_info); $course = $cb->build($source_session_id, $source_course_code, $withBaseContent); $restorer = new CourseRestorer($course); + $restorer->copySessionContent = $copySessionContent; $restorer->skip_content = $params; $restorer->restore( $destination_course_code, @@ -4260,7 +4262,7 @@ class CourseManager $source_session_id = 0, $destination_session_id = 0, $params = [], - $copySessionContent = false + bool $copySessionContent = false ) { $source_course_info = api_get_course_info($source_course_code); if (!empty($source_course_info)) { @@ -4278,7 +4280,8 @@ class CourseManager $newCourse->getCode(), $destination_session_id, $params, - true + true, + $copySessionContent ); if ($result) { return $newCourse; diff --git a/public/main/inc/lib/promotion.lib.php b/public/main/inc/lib/promotion.lib.php index a67956fd8e..1b81c3efbb 100644 --- a/public/main/inc/lib/promotion.lib.php +++ b/public/main/inc/lib/promotion.lib.php @@ -104,7 +104,7 @@ class Promotion extends Model foreach ($session_list as $item) { $sid = SessionManager::copy( - $item['id'], + (int) $item['id'], true, false, false, diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index 17e4f3ffd1..074dfaa439 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -4592,6 +4592,7 @@ class SessionManager * @param bool $copyTeachersAndDrh * @param bool $create_new_courses New courses will be created * @param bool $set_exercises_lp_invisible Set exercises and LPs in the new session to invisible by default + * @param bool $copyWithSessionContent Copy course session content into the courses * * @return int The new session ID on success, 0 otherwise * @@ -4600,11 +4601,12 @@ class SessionManager * @todo make sure the extra session fields are copied too */ public static function copy( - $id, - $copy_courses = true, - $copyTeachersAndDrh = true, - $create_new_courses = false, - $set_exercises_lp_invisible = false + int $id, + bool $copy_courses = true, + bool $copyTeachersAndDrh = true, + bool $create_new_courses = false, + bool $set_exercises_lp_invisible = false, + bool $copyWithSessionContent = false, ) { $id = (int) $id; $s = self::fetch($id); @@ -4714,9 +4716,7 @@ class SessionManager foreach ($short_courses as $course_data) { $course = CourseManager::copy_course_simple( - $course_data['title'].' '.get_lang( - 'Copy' - ), + $course_data['title'].' '.get_lang('Copy'), $course_data['course_code'], $id, $sid, @@ -4765,6 +4765,20 @@ class SessionManager $short_courses = $new_short_courses; self::add_courses_to_session($sid, $short_courses, true); + if ($copyWithSessionContent) { + foreach ($courses as $course) { + CourseManager::copy_course( + $course['code'], + $id, + $course['code'], + $sid, + [], + false, + true + ); + } + } + if (false === $create_new_courses && $copyTeachersAndDrh) { foreach ($short_courses as $courseItemId) { $coachList = self::getCoachesByCourseSession($id, $courseItemId); diff --git a/public/main/session/session_list.php b/public/main/session/session_list.php index 1ebe175b83..d6d9fb96a2 100644 --- a/public/main/session/session_list.php +++ b/public/main/session/session_list.php @@ -20,7 +20,12 @@ $action = $_REQUEST['action'] ?? null; $idChecked = $_REQUEST['idChecked'] ?? null; $idMultiple = $_REQUEST['id'] ?? null; $listType = isset($_REQUEST['list_type']) ? Security::remove_XSS($_REQUEST['list_type']) : SessionManager::getDefaultSessionTab(); -$copySessionContent = isset($_REQUEST['copy_session_content']) ? true : false; +$copySessionContent = isset($_REQUEST['copy_session_content']); +$addSessionContent = 'true' === api_get_setting('session.duplicate_specific_session_content_on_session_copy'); + +if (!$addSessionContent) { + $copySessionContent = false; +} switch ($action) { case 'delete_multiple': @@ -50,7 +55,14 @@ switch ($action) { header('Location: '.$url); exit(); case 'copy': - $result = SessionManager::copy($idChecked); + $result = SessionManager::copy( + (int) $idChecked, + true, + true, + false, + false, + $copySessionContent + ); if ($result) { Display::addFlash(Display::return_message(get_lang('ItemCopied'))); } else { @@ -65,7 +77,7 @@ switch ($action) { case 'copy_multiple': $sessionList = explode(',', $idMultiple); foreach ($sessionList as $id) { - $sessionIdCopied = SessionManager::copy($id); + $sessionIdCopied = SessionManager::copy((int) $id); if ($sessionIdCopied) { $sessionInfo = api_get_session_info($sessionIdCopied); Display::addFlash(Display::return_message(get_lang('ItemCopied').' - '.$sessionInfo['name'])); @@ -152,14 +164,38 @@ if (isset($_REQUEST['keyword'])) { $filter->groupOp = 'OR'; $filter = json_encode($filter); - $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&_force_search=true&rows=20&page=1&sidx=&sord=asc&filters='.$filter.'&searchField=s.title&searchString='.Security::remove_XSS($_REQUEST['keyword']).'&searchOper=in'; + $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?' + .http_build_query([ + 'a' => 'get_sessions', + '_force_search' => 'true', + 'rows' => 20, + 'page' => 1, + 'sidx' => '', + 'sord' => 'asc', + 'filters' => $filter, + 'searchField' => 's.title', + 'searchString' => Security::remove_XSS($_REQUEST['keyword']), + 'searchOper' => 'in', + ]); } if (isset($_REQUEST['id_category'])) { $sessionCategory = SessionManager::get_session_category($_REQUEST['id_category']); if (!empty($sessionCategory)) { //Begin with see the searchOper param - $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&_force_search=true&rows=20&page=1&sidx=&sord=asc&filters=&searchField=sc.title&searchString='.Security::remove_XSS($sessionCategory['title']).'&searchOper=in'; + $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?' + .http_build_query([ + 'a' => 'get_sessions', + '_force_search' => 'true', + 'rows' => 20, + 'page' => 1, + 'sidx' => '', + 'sord' => 'asc', + 'filters' => '', + 'searchField' => 'sc.title', + 'searchString' => Security::remove_XSS($sessionCategory['title']), + 'searchOper' => 'in', + ]); } } @@ -187,6 +223,11 @@ if (!isset($_GET['keyword'])) { } $hideSearch = ('true' === api_get_setting('session.hide_search_form_in_session_list')); +$copySessionContentLink = ''; +if ($addSessionContent) { + $copySessionContentLink = ' '. + Display::return_icon('copy.png', get_lang('CopyWithSessionContent'), '', ICON_SIZE_SMALL).''; +} //With this function we can add actions to the jgrid (edit, delete, etc) $action_links = 'function action_formatter(cellvalue, options, rowObject) { @@ -194,6 +235,7 @@ $action_links = 'function action_formatter(cellvalue, options, rowObject) { ' '.Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', null, 22, get_lang('Subscribe users to this session')).''. ' '.Display::getMdiIcon('book-open-page-variant', 'ch-tool-icon', null, 22, get_lang('Add courses to this session')).''. ' '.Display::getMdiIcon('text-box-plus', 'ch-tool-icon', null, 22, get_lang('Copy')).''. + $copySessionContentLink. ''. '\'; }'; diff --git a/src/CoreBundle/Settings/SessionSettingsSchema.php b/src/CoreBundle/Settings/SessionSettingsSchema.php index d5f2da7dbc..afea86fc27 100644 --- a/src/CoreBundle/Settings/SessionSettingsSchema.php +++ b/src/CoreBundle/Settings/SessionSettingsSchema.php @@ -79,6 +79,7 @@ class SessionSettingsSchema extends AbstractSettingsSchema 'session_creation_user_course_extra_field_relation_to_prefill' => '', 'session_creation_form_set_extra_fields_mandatory' => '', 'session_model_list_field_ordered_by_id' => 'false', + 'duplicate_specific_session_content_on_session_copy' => 'false', ] ) ; @@ -217,6 +218,7 @@ class SessionSettingsSchema extends AbstractSettingsSchema ] ) ->add('session_model_list_field_ordered_by_id', YesNoType::class) + ->add('duplicate_specific_session_content_on_session_copy', YesNoType::class) ; $this->updateFormFieldsFromSettingsInfo($builder); diff --git a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php index a9379dc78d..675d0bbd50 100644 --- a/src/CourseBundle/Component/CourseCopy/CourseRestorer.php +++ b/src/CourseBundle/Component/CourseCopy/CourseRestorer.php @@ -80,6 +80,7 @@ class CourseRestorer */ public $add_text_in_items = false; public $destination_course_id; + public bool $copySessionContent = false; /** * CourseRestorer constructor. @@ -392,7 +393,7 @@ class CourseRestorer $groupInfo = $this->checkGroupId($groupId); // if folder exists then just refresh it - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $documentData, @@ -403,12 +404,13 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } } } elseif (DOCUMENT == $document->file_type) { // Checking if folder exists in the database otherwise we created it $dir_to_create = dirname($document->path); + $originalFolderNameList[basename($document->path)] = $document->title; if (!empty($dir_to_create) && 'document' != $dir_to_create && '/' != $dir_to_create) { if (is_dir($path.dirname($document->path))) { $sql = "SELECT id FROM $table @@ -510,7 +512,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $document_id, @@ -521,7 +523,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } else { $obj = Database::fetch_object($res); $document_id = $obj->id; @@ -557,7 +559,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $obj->id, @@ -568,7 +570,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } // Replace old course code with the new destination code @@ -637,6 +639,7 @@ class CourseRestorer } if (!empty($session_id)) { + $originalPath = $document->path; $document_path = explode('/', $document->path, 3); $course_path = $path; $orig_base_folder = $document_path[1]; @@ -658,12 +661,14 @@ class CourseRestorer if ($folder_exists) { // e.g: carpeta1 in session $_SESSION['orig_base_foldername'] = $new_base_foldername; - $x = ''; + $x = 0; while ($folder_exists) { $x++; $new_base_foldername = $document_path[1].'_'.$x; $new_base_path = $orig_base_path.'_'.$x; - if ($_SESSION['new_base_foldername'] == $new_base_foldername) { + if (isset($_SESSION['new_base_foldername']) + && $_SESSION['new_base_foldername'] == $new_base_foldername + ) { break; } $folder_exists = file_exists($new_base_path); @@ -749,7 +754,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $document_id, @@ -760,7 +765,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } } else { if (file_exists($path.$document->path)) { @@ -813,7 +818,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $document_id, @@ -824,7 +829,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } } } else { @@ -879,7 +884,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $document_id, @@ -890,7 +895,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } } @@ -957,7 +962,7 @@ class CourseRestorer $toUserId = $this->checkUserId($toUserId, true); $groupInfo = $this->checkGroupId($toGroupId); - api_item_property_update( + /*api_item_property_update( $course_info, TOOL_DOCUMENT, $document_id, @@ -968,7 +973,7 @@ class CourseRestorer null, null, $my_session_id - ); + );*/ } } else { // There was an error in checking existence and @@ -1116,10 +1121,7 @@ class CourseRestorer if (isset($this->course->resources[RESOURCE_FORUMCATEGORY]) && isset($this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']])) { if (-1 == $this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']]->destination_id) { - $cat_id = $this->restore_forum_category( - $params['forum_category'], - $sessionId - ); + $cat_id = $this->restore_forum_category($params['forum_category'], $sessionId); } else { $cat_id = $this->course->resources[RESOURCE_FORUMCATEGORY][$params['forum_category']]->destination_id; } @@ -1157,13 +1159,13 @@ class CourseRestorer $sql = "UPDATE $table_forum SET forum_id = iid WHERE iid = $new_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_FORUM, $new_id, 'ForumUpdated', api_get_user_id() - ); + );*/ $this->course->resources[RESOURCE_FORUM][$id]->destination_id = $new_id; @@ -1224,13 +1226,13 @@ class CourseRestorer $sql = "UPDATE $forum_cat_table SET cat_id = iid WHERE iid = $new_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_FORUM_CATEGORY, $new_id, 'ForumCategoryUpdated', api_get_user_id() - ); + );*/ $this->course->resources[RESOURCE_FORUMCATEGORY][$id]->destination_id = $new_id; } @@ -1276,7 +1278,7 @@ class CourseRestorer $sql = "UPDATE $table SET thread_id = iid WHERE iid = $new_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_FORUM_THREAD, $new_id, @@ -1287,7 +1289,7 @@ class CourseRestorer null, null, $sessionId - ); + );*/ $this->course->resources[RESOURCE_FORUMTOPIC][$thread_id]->destination_id = $new_id; foreach ($this->course->resources[RESOURCE_FORUMPOST] as $post_id => $post) { @@ -1335,7 +1337,7 @@ class CourseRestorer $sql = "UPDATE $table_post SET post_id = iid WHERE iid = $new_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_FORUM_POST, $new_id, @@ -1346,7 +1348,7 @@ class CourseRestorer null, null, $sessionId - ); + );*/ $this->course->resources[RESOURCE_FORUMPOST][$id]->destination_id = $new_id; } @@ -1363,15 +1365,12 @@ class CourseRestorer $resources = $this->course->resources; foreach ($resources[RESOURCE_LINK] as $oldLinkId => $link) { - $cat_id = $this->restore_link_category( - $link->category_id, - $session_id - ); + $cat_id = (int) $this->restore_link_category($link->category_id, $session_id); $sql = "SELECT MAX(display_order) FROM $link_table WHERE c_id = ".$this->destination_course_id." AND - category_id='".(int) $cat_id."'"; + category_id='".$cat_id."'"; $result = Database::query($sql); list($max_order) = Database::fetch_array($result); @@ -1395,13 +1394,13 @@ class CourseRestorer $sql = "UPDATE $link_table SET id = iid WHERE iid = $id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_LINK, $id, 'LinkAdded', api_get_user_id() - ); + );*/ if (!isset($this->course->resources[RESOURCE_LINK][$oldLinkId])) { $this->course->resources[RESOURCE_LINK][$oldLinkId] = new stdClass(); @@ -1454,13 +1453,13 @@ class CourseRestorer Database::query($sql); $courseInfo = api_get_course_info_by_id($this->destination_course_id); - api_item_property_update( + /*api_item_property_update( $courseInfo, TOOL_LINK_CATEGORY, $new_id, 'LinkCategoryAdded', api_get_user_id() - ); + );*/ api_set_default_visibility( $new_id, TOOL_LINK_CATEGORY, @@ -1489,11 +1488,13 @@ class CourseRestorer $tool_intro_table = Database::get_course_table(TABLE_TOOL_INTRO); $resources = $this->course->resources; foreach ($resources[RESOURCE_TOOL_INTRO] as $id => $tool_intro) { - $sql = "DELETE FROM $tool_intro_table + if (!$this->copySessionContent) { + $sql = "DELETE FROM $tool_intro_table WHERE c_id = ".$this->destination_course_id." AND id='".self::DBUTF8escapestring($tool_intro->id)."'"; - Database::query($sql); + Database::query($sql); + } $tool_intro->intro_text = DocumentManager::replaceUrlWithNewCourseCode( $tool_intro->intro_text, @@ -2862,7 +2863,7 @@ class CourseRestorer $extraFieldValue->save($params); } } - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_LEARNPATH, $new_lp_id, @@ -2873,10 +2874,10 @@ class CourseRestorer 0, 0, $session_id - ); + );*/ // Set the new LP to visible - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_LEARNPATH, $new_lp_id, @@ -2887,7 +2888,7 @@ class CourseRestorer 0, 0, $session_id - ); + );*/ $new_item_ids = []; $parent_item_ids = []; @@ -2904,7 +2905,7 @@ class CourseRestorer // Dealing with path the same way as ref as some data has // been put into path when it's a local resource // Only fix the path for no scos - if ('sco' == $item['item_type']) { + if ('sco' === $item['item_type']) { $path = $item['path']; } else { $path = $this->get_new_id($item['item_type'], $item['path']); @@ -3121,13 +3122,13 @@ class CourseRestorer $sql = "UPDATE $table_glossary SET glossary_id = iid WHERE iid = $my_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_GLOSSARY, $my_id, 'GlossaryAdded', api_get_user_id() - ); + );*/ if (!isset($this->course->resources[RESOURCE_GLOSSARY][$id])) { $this->course->resources[RESOURCE_GLOSSARY][$id] = new stdClass(); @@ -3255,13 +3256,13 @@ class CourseRestorer $sql = "UPDATE $table_thematic SET id = iid WHERE iid = $last_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, 'thematic', $last_id, 'ThematicAdded', api_get_user_id() - ); + );*/ foreach ($thematic->thematic_advance_list as $thematic_advance) { unset($thematic_advance['id']); @@ -3280,13 +3281,13 @@ class CourseRestorer $sql = "UPDATE $table_thematic_advance SET id = iid WHERE iid = $my_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, 'thematic_advance', $my_id, 'ThematicAdvanceAdded', api_get_user_id() - ); + );*/ } } @@ -3301,13 +3302,13 @@ class CourseRestorer $sql = "UPDATE $table_thematic_plan SET id = iid WHERE iid = $my_id"; Database::query($sql); - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, 'thematic_plan', $my_id, 'ThematicPlanAdded', api_get_user_id() - ); + );*/ } } } @@ -3348,13 +3349,13 @@ class CourseRestorer $this->course->resources[RESOURCE_ATTENDANCE][$id]->destination_id = $last_id; - api_item_property_update( + /*api_item_property_update( $this->destination_course_info, TOOL_ATTENDANCE, $last_id, 'AttendanceAdded', api_get_user_id() - ); + );*/ foreach ($obj->attendance_calendar as $attendance_calendar) { unset($attendance_calendar['id']); From 6705a874d9167eb544f16174ee57c206d2c599e8 Mon Sep 17 00:00:00 2001 From: BorjaSanchezBeezNest Date: Thu, 23 Jan 2025 14:53:38 +0100 Subject: [PATCH 09/33] Tracking: Add quarterly report in admin statistics page --- public/main/admin/statistics/index.php | 153 +++- public/main/inc/ajax/statistics.ajax.php | 654 +++++++++++++++++- public/main/inc/lib/api.lib.php | 17 + .../main/inc/lib/internationalization.lib.php | 55 ++ public/main/inc/lib/statistics.lib.php | 180 ++++- public/main/inc/lib/tracking.lib.php | 36 + public/main/inc/lib/usermanager.lib.php | 139 +++- .../block_global_info.class.php | 10 +- 8 files changed, 1185 insertions(+), 59 deletions(-) diff --git a/public/main/admin/statistics/index.php b/public/main/admin/statistics/index.php index 602e99ee84..b5d62d9667 100644 --- a/public/main/admin/statistics/index.php +++ b/public/main/admin/statistics/index.php @@ -352,16 +352,17 @@ $tools = [ 'report=new_user_registrations' => get_lang('New users registrations'), ], get_lang('System') => [ - 'report=activities' => get_lang('ImportantActivities'), - 'report=user_session' => get_lang('PortalUserSessionStats'), + 'report=activities' => get_lang('Important activities'), + 'report=user_session' => get_lang('Portal user session stats'), + 'report=quarterly_report' => get_lang('Quarterly report'), ], get_lang('Social') => [ - 'report=messagereceived' => get_lang('MessagesReceived'), - 'report=messagesent' => get_lang('MessagesSent'), - 'report=friends' => get_lang('CountFriends'), + 'report=messagereceived' => get_lang('Number of messages received'), + 'report=messagesent' => get_lang('Number of messages sent'), + 'report=friends' => get_lang('Contacts count'), ], get_lang('Session') => [ - 'report=session_by_date' => get_lang('SessionsByDate'), + 'report=session_by_date' => get_lang('Sessions by date'), ], ]; @@ -1665,6 +1666,146 @@ switch ($report) { case 'logins_by_date': $content .= Statistics::printLoginsByDate(); break; + case 'quarterly_report': + global $htmlHeadXtra; + $ajaxPath = api_get_path(WEB_AJAX_PATH); + $waitIcon = Display::getMdiIcon('clock-time-four', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, false); + $htmlHeadXtra[] .= ''; + $htmlHeadXtra[] .= ''; + $htmlHeadXtra[] .= ''; + $htmlHeadXtra[] .= ''; + $htmlHeadXtra[] .= ''; + $htmlHeadXtra[] .= ''; + if (api_get_current_access_url_id() === 1) { + $htmlHeadXtra[] .= ''; + } + $content .= Display::tag('H4', get_lang('Number of users registered and connected'), ['style' => 'margin-bottom: 25px;']); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyUsers();', 'class' => 'btn btn-default'] + ); + $content .= Display::div('', ['id' => 'tracking-report-quarterly-users', 'style' => 'margin: 30px;']); + $content .= Display::tag('H4', get_lang('Number of existing and available courses'), ['style' => 'margin-bottom: 25px;']); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyCourses();', 'class' => 'btn btn-default'] + ); + $content .= Display::div('', ['id' => 'tracking-report-quarterly-courses', 'style' => 'margin: 30px;']); + $content .= Display::tag('H4', get_lang('Hours of training'), ['style' => 'margin-bottom: 25px;']); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyHoursOfTraining();', 'class' => 'btn btn-default'] + ); + $content .= Display::div( + '', + [ + 'id' => 'tracking-report-quarterly-hours-of-training', + 'style' => 'margin: 30px;', + ] + ); + $content .= Display::tag( + 'H4', + get_lang('Number of certificates generated'), + ['style' => 'margin-bottom: 25px;'] + ); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyCertificatesGenerated();', 'class' => 'btn btn-default'] + ); + $content .= Display::div( + '', + ['id' => 'tracking-report-quarterly-number-of-certificates-generated', 'style' => 'margin: 30px;'] + ); + $content .= Display::tag( + 'H4', + get_lang('Number of sessions per duration'), + ['style' => 'margin-bottom: 25px;'] + ); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlySessionsByDuration();', 'class' => 'btn btn-default'] + ); + $content .= Display::div( + '', + ['id' => 'tracking-report-quarterly-sessions-by-duration', 'style' => 'margin: 30px;'] + ); + $content .= Display::tag( + 'H4', + get_lang('Number of courses, sessions and subscribed users'), + ['style' => 'margin-bottom: 25px;'] + ); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyCoursesAndSessions();', 'class' => 'btn btn-default'] + ); + $content .= Display::div( + '', + [ + 'id' => 'tracking-report-quarterly-courses-and-sessions', + 'style' => 'margin: 30px;', + ] + ); + if (api_get_current_access_url_id() === 1) { + $content .= Display::tag( + 'H4', + get_lang('Total disk usage'), + ['style' => 'margin-bottom: 25px;'] + ); + $content .= Display::url( + get_lang('Show'), + 'javascript://', + ['onclick' => 'loadReportQuarterlyTotalDiskUsage();', 'class' => 'btn btn-default'] + ); + $content .= Display::div( + '', + [ + 'id' => 'tracking-report-quarterly-total-disk-usage', + 'style' => 'margin: 30px;', + ] + ); + } + break; } Display::display_header($tool_name); diff --git a/public/main/inc/ajax/statistics.ajax.php b/public/main/inc/ajax/statistics.ajax.php index 83ba57dcf2..7504b8dbdf 100644 --- a/public/main/inc/ajax/statistics.ajax.php +++ b/public/main/inc/ajax/statistics.ajax.php @@ -644,7 +644,7 @@ switch ($action) { $all = []; while ($row = Database::fetch_array($result)) { $categoryData = SessionManager::get_session_category($row['session_category_id']); - $label = get_lang('NoCategory'); + $label = get_lang('Without category'); if ($categoryData) { $label = $categoryData['name']; } @@ -759,4 +759,656 @@ switch ($action) { header('Content-type: application/json'); echo json_encode($list); break; + case 'report_quarterly_users': + $currentQuarterDates = getQuarterDates(); + $pre1QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-3 month') + ->format('Y-m-d') + ); + $pre2QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-6 month') + ->format('Y-m-d') + ); + $pre3QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-9 month') + ->format('Y-m-d') + ); + $pre4QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-12 month') + ->format('Y-m-d') + ); + $pre5QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-15 month') + ->format('Y-m-d') + ); + // Make de headers for the table + $headers = [ + '', + $pre5QuarterDates['quarter_title'], + $pre4QuarterDates['quarter_title'], + $pre3QuarterDates['quarter_title'], + $pre2QuarterDates['quarter_title'], + $pre1QuarterDates['quarter_title'], + get_lang('YoY'), + $currentQuarterDates['quarter_title'].'*', + ]; + // Get the data for the number of user registered row (2) + $countUsersTotal = UserManager::get_number_of_users( + null, + null, + null + ); + $countUsersPre1Quarter = UserManager::get_number_of_users( + null, + null, + null, + null, + $pre1QuarterDates['quarter_end'] + ); + $countUsersPre2Quarter = UserManager::get_number_of_users( + null, + null, + null, + null, + $pre2QuarterDates['quarter_end'] + ); + $countUsersPre3Quarter = UserManager::get_number_of_users( + null, + null, + null, + null, + $pre3QuarterDates['quarter_end'] + ); + $countUsersPre4Quarter = UserManager::get_number_of_users( + null, + null, + null, + null, + $pre4QuarterDates['quarter_end'] + ); + $countUsersPre5Quarter = UserManager::get_number_of_users( + null, + null, + null, + null, + $pre5QuarterDates['quarter_end'] + ); + // Calculate percent for first row + $percentIncrementUsersRegistered = api_calculate_increment_percent( + $countUsersPre1Quarter, + $countUsersPre5Quarter + ); + // Get the data for number of users connected row (3) + $countUsersConnectedCurrentQuarter = count( + Statistics::getLoginsByDate( + $currentQuarterDates['quarter_start'], + $currentQuarterDates['quarter_end'] + ) + ); + $countUsersConnectedPre1Quarter = count( + Statistics::getLoginsByDate( + $pre1QuarterDates['quarter_start'], + $pre1QuarterDates['quarter_end'] + ) + ); + $countUsersConnectedPre2Quarter = count( + Statistics::getLoginsByDate( + $pre2QuarterDates['quarter_start'], + $pre2QuarterDates['quarter_end'] + ) + ); + $countUsersConnectedPre3Quarter = count( + Statistics::getLoginsByDate( + $pre3QuarterDates['quarter_start'], + $pre3QuarterDates['quarter_end'] + ) + ); + $countUsersConnectedPre4Quarter = count( + Statistics::getLoginsByDate( + $pre4QuarterDates['quarter_start'], + $pre4QuarterDates['quarter_end'] + ) + ); + $countUsersConnectedPre5Quarter = count( + Statistics::getLoginsByDate( + $pre5QuarterDates['quarter_start'], + $pre5QuarterDates['quarter_end'] + ) + ); + // Calculate percent for second row + $percentIncrementUsersConnected = api_calculate_increment_percent( + $countUsersConnectedPre1Quarter, + $countUsersConnectedPre5Quarter + ); + //Make de rows with the recollected data + $rows = []; + $rows[] = [ + get_lang('Number of users registered (total)'), + $countUsersPre5Quarter, + $countUsersPre4Quarter, + $countUsersPre3Quarter, + $countUsersPre2Quarter, + $countUsersPre1Quarter, + $percentIncrementUsersRegistered, + $countUsersTotal, + ]; + //todo comprobacion + - + $rows[] = [ + get_lang('Number of users registered (new vs previous quarter)'), + '-', + '+'.($countUsersPre1Quarter - $countUsersPre2Quarter), + '+'.($countUsersPre2Quarter - $countUsersPre3Quarter), + '+'.($countUsersPre3Quarter - $countUsersPre4Quarter), + '+'.($countUsersPre4Quarter - $countUsersPre5Quarter), + '-', + '+'.($countUsersTotal - $countUsersPre1Quarter), + ]; + $rows[] = [ + get_lang('Number of users who connected'), + $countUsersConnectedPre5Quarter, + $countUsersConnectedPre4Quarter, + $countUsersConnectedPre3Quarter, + $countUsersConnectedPre2Quarter, + $countUsersConnectedPre1Quarter, + $percentIncrementUsersConnected, + $countUsersConnectedCurrentQuarter, + ]; + echo Display::table($headers, $rows, []); + echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning'); + break; + case 'report_quarterly_courses': + $currentQuarterDates = getQuarterDates(); + $pre1QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-3 month') + ->format('Y-m-d') + ); + $pre2QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-6 month') + ->format('Y-m-d') + ); + $pre3QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-9 month') + ->format('Y-m-d') + ); + $pre4QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-12 month') + ->format('Y-m-d') + ); + $pre5QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-15 month') + ->format('Y-m-d') + ); + // Make the headers for the table + $headers = [ + '', + $pre5QuarterDates['quarter_title'], + $pre4QuarterDates['quarter_title'], + $pre3QuarterDates['quarter_title'], + $pre2QuarterDates['quarter_title'], + $pre1QuarterDates['quarter_title'], + get_lang('YoY'), + $currentQuarterDates['quarter_title'].'*', + ]; + // Get the data for the rows + $countCoursesCurrentQuarter = Statistics::countCourses(null, null, null); + $countCoursesPre1Quarter = Statistics::countCourses(null, null, $pre1QuarterDates['quarter_end']); + $countCoursesPre2Quarter = Statistics::countCourses(null, null, $pre2QuarterDates['quarter_end']); + $countCoursesPre3Quarter = Statistics::countCourses(null, null, $pre3QuarterDates['quarter_end']); + $countCoursesPre4Quarter = Statistics::countCourses(null, null, $pre4QuarterDates['quarter_end']); + $countCoursesPre5Quarter = Statistics::countCourses(null, null, $pre5QuarterDates['quarter_end']); + $auxArrayVisibilities = [ + COURSE_VISIBILITY_OPEN_WORLD, + COURSE_VISIBILITY_OPEN_PLATFORM, + COURSE_VISIBILITY_REGISTERED, + ]; + $countCoursesAvailableCurrentQuarter = Statistics::countCoursesByVisibility($auxArrayVisibilities); + $countCoursesAvailablePre1Quarter = Statistics::countCoursesByVisibility( + $auxArrayVisibilities, + null, + $pre1QuarterDates['quarter_end'] + ); + $countCoursesAvailablePre2Quarter = Statistics::countCoursesByVisibility( + $auxArrayVisibilities, + null, + $pre2QuarterDates['quarter_end'] + ); + $countCoursesAvailablePre3Quarter = Statistics::countCoursesByVisibility( + $auxArrayVisibilities, + null, + $pre3QuarterDates['quarter_end'] + ); + $countCoursesAvailablePre4Quarter = Statistics::countCoursesByVisibility( + $auxArrayVisibilities, + null, + $pre4QuarterDates['quarter_end'] + ); + $countCoursesAvailablePre5Quarter = Statistics::countCoursesByVisibility( + $auxArrayVisibilities, + null, + $pre5QuarterDates['quarter_end'] + ); + // Calculate percents for first row + $percentIncrementCourses = api_calculate_increment_percent( + $countCoursesPre1Quarter, + $countCoursesPre5Quarter + ); + // Calculate percents for second row + $percentIncrementUsersRegistered = api_calculate_increment_percent( + $countCoursesAvailablePre1Quarter, + $countCoursesAvailablePre5Quarter + ); + //Make the rows with the recollected data + $rows = []; + $rows[] = [ + get_lang('Number of existing courses (total)'), + $countCoursesPre5Quarter, + $countCoursesPre4Quarter, + $countCoursesPre3Quarter, + $countCoursesPre2Quarter, + $countCoursesPre1Quarter, + $percentIncrementCourses, + $countCoursesCurrentQuarter, + ]; + $rows[] = [ + get_lang('Number of available courses (not closed or hidden, total)'), + $countCoursesAvailablePre5Quarter, + $countCoursesAvailablePre4Quarter, + $countCoursesAvailablePre3Quarter, + $countCoursesAvailablePre2Quarter, + $countCoursesAvailablePre1Quarter, + $percentIncrementUsersRegistered, + $countCoursesAvailableCurrentQuarter, + ]; + echo Display::table($headers, $rows, []); + echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning'); + break; + case 'report_quarterly_hours_of_training': + $currentQuarterDates = getQuarterDates(); + $pre1QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-3 month') + ->format('Y-m-d') + ); + $pre2QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-6 month') + ->format('Y-m-d') + ); + $pre3QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-9 month') + ->format('Y-m-d') + ); + $pre4QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-12 month') + ->format('Y-m-d') + ); + $pre5QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-15 month') + ->format('Y-m-d') + ); + // Make the headers for the table + $headers = [ + '', + $pre5QuarterDates['quarter_title'], + $pre4QuarterDates['quarter_title'], + $pre3QuarterDates['quarter_title'], + $pre2QuarterDates['quarter_title'], + $pre1QuarterDates['quarter_title'], + get_lang('YoY'), + $currentQuarterDates['quarter_title'].'*', + ]; + // Get data for the row + $timeSpentCoursesCurrentQuarter = Tracking::getTotalTimeSpentInCourses( + $currentQuarterDates['quarter_start'], + $currentQuarterDates['quarter_end'] + ); + $timeSpentCourses1PreQuarter = Tracking::getTotalTimeSpentInCourses( + $pre1QuarterDates['quarter_start'], + $pre1QuarterDates['quarter_end'] + ); + $timeSpentCourses2PreQuarter = Tracking::getTotalTimeSpentInCourses( + $pre2QuarterDates['quarter_start'], + $pre2QuarterDates['quarter_end'] + ); + $timeSpentCourses3PreQuarter = Tracking::getTotalTimeSpentInCourses( + $pre3QuarterDates['quarter_start'], + $pre3QuarterDates['quarter_end'] + ); + $timeSpentCourses4PreQuarter = Tracking::getTotalTimeSpentInCourses( + $pre4QuarterDates['quarter_start'], + $pre4QuarterDates['quarter_end'] + ); + $timeSpentCourses5PreQuarter = Tracking::getTotalTimeSpentInCourses( + $pre5QuarterDates['quarter_start'], + $pre5QuarterDates['quarter_end'] + ); + // Calculate percent for the row + $percentIncrementTimeSpent = api_calculate_increment_percent( + $timeSpentCourses1PreQuarter, + $timeSpentCourses5PreQuarter + ); + //Make the row with the recollected data + $rows = []; + $rows[] = [ + get_lang('Number of hours of training followed (total)'), + $timeSpentCourses5PreQuarter, + $timeSpentCourses4PreQuarter, + $timeSpentCourses3PreQuarter, + $timeSpentCourses2PreQuarter, + $timeSpentCourses1PreQuarter, + $percentIncrementTimeSpent, + $timeSpentCoursesCurrentQuarter, + ]; + echo Display::table($headers, $rows, []); + echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning'); + break; + case 'report_quarterly_number_of_certificates_generated': + $currentQuarterDates = getQuarterDates(); + $pre1QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-3 month') + ->format('Y-m-d') + ); + $pre2QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-6 month') + ->format('Y-m-d') + ); + $pre3QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-9 month') + ->format('Y-m-d') + ); + $pre4QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-12 month') + ->format('Y-m-d') + ); + $pre5QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-15 month') + ->format('Y-m-d') + ); + // Make the headers for the table + $headers = [ + '', + $pre5QuarterDates['quarter_title'], + $pre4QuarterDates['quarter_title'], + $pre3QuarterDates['quarter_title'], + $pre2QuarterDates['quarter_title'], + $pre1QuarterDates['quarter_title'], + get_lang('YoY'), + $currentQuarterDates['quarter_title'].'*', + ]; + // Get data for the row + $certificateGeneratedCurrentQuarter = Statistics::countCertificatesByQuarter( + null, + $currentQuarterDates['quarter_end'] + ); + $certificateGenerated1PreQuarter = Statistics::countCertificatesByQuarter( + null, + $pre1QuarterDates['quarter_end'] + ); + $certificateGenerated2PreQuarter = Statistics::countCertificatesByQuarter( + null, + $pre2QuarterDates['quarter_end'] + ); + $certificateGenerated3PreQuarter = Statistics::countCertificatesByQuarter( + null, + $pre3QuarterDates['quarter_end'] + ); + $certificateGenerated4PreQuarter = Statistics::countCertificatesByQuarter( + null, + $pre4QuarterDates['quarter_end'] + ); + $certificateGenerated5PreQuarter = Statistics::countCertificatesByQuarter( + null, + $pre5QuarterDates['quarter_end'] + ); + // Calculate percent for the row + $percentIncrementCertificateGenerated = api_calculate_increment_percent( + $certificateGenerated1PreQuarter, + $certificateGenerated5PreQuarter + ); + //Make the row with the recollected data + $rows = []; + $rows[] = [ + get_lang('Number of certificates generated'), + $certificateGenerated5PreQuarter, + $certificateGenerated4PreQuarter, + $certificateGenerated3PreQuarter, + $certificateGenerated2PreQuarter, + $certificateGenerated1PreQuarter, + $percentIncrementCertificateGenerated, + $certificateGeneratedCurrentQuarter, + ]; + echo Display::table($headers, $rows, []); + echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning'); + break; + case "report_quarterly_sessions_by_duration": + $currentQuarterDates = getQuarterDates(); + $pre1QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-3 month') + ->format('Y-m-d') + ); + $pre2QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-6 month') + ->format('Y-m-d') + ); + $pre3QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-9 month') + ->format('Y-m-d') + ); + $pre4QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-12 month') + ->format('Y-m-d') + ); + $pre5QuarterDates = getQuarterDates( + date_create($currentQuarterDates['quarter_start']) + ->modify('-15 month') + ->format('Y-m-d') + ); + // Make the headers for the table + $headers = [ + get_lang('Sessions per duration (by quarter)'), + $pre5QuarterDates['quarter_title'], + $pre4QuarterDates['quarter_title'], + $pre3QuarterDates['quarter_title'], + $pre2QuarterDates['quarter_title'], + $pre1QuarterDates['quarter_title'], + get_lang('YoY'), + $currentQuarterDates['quarter_title'].'*', + ]; + // Get the data for the rows + $sessionsDurationCurrentQuarter = Statistics::getSessionsByDuration( + $currentQuarterDates['quarter_start'], + $currentQuarterDates['quarter_end'] + ); + $sessionsDuration1PreQuarter = Statistics::getSessionsByDuration( + $pre1QuarterDates['quarter_start'], + $pre1QuarterDates['quarter_end'] + ); + $sessionsDuration2PreQuarter = Statistics::getSessionsByDuration( + $pre2QuarterDates['quarter_start'], + $pre2QuarterDates['quarter_end'] + ); + $sessionsDuration3PreQuarter = Statistics::getSessionsByDuration( + $pre3QuarterDates['quarter_start'], + $pre3QuarterDates['quarter_end'] + ); + $sessionsDuration4PreQuarter = Statistics::getSessionsByDuration( + $pre4QuarterDates['quarter_start'], + $pre4QuarterDates['quarter_end'] + ); + $sessionsDuration5PreQuarter = Statistics::getSessionsByDuration( + $pre5QuarterDates['quarter_start'], + $pre5QuarterDates['quarter_end'] + ); + // Calculate percent for the rows + $percentIncrementSessionDuration0 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['0'], + $sessionsDuration5PreQuarter['0'] + ); + $percentIncrementSessionDuration5 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['5'], + $sessionsDuration5PreQuarter['5'] + ); + $percentIncrementSessionDuration10 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['10'], + $sessionsDuration5PreQuarter['10'] + ); + $percentIncrementSessionDuration15 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['15'], + $sessionsDuration5PreQuarter['15'] + ); + $percentIncrementSessionDuration30 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['30'], + $sessionsDuration5PreQuarter['30'] + ); + $percentIncrementSessionDuration60 = api_calculate_increment_percent( + $sessionsDuration1PreQuarter['60'], + $sessionsDuration5PreQuarter['60'] + ); + //Make the rows with the recollected data + $rows = []; + $rows[] = [ + '0-5′', + $sessionsDuration5PreQuarter['0'], + $sessionsDuration4PreQuarter['0'], + $sessionsDuration3PreQuarter['0'], + $sessionsDuration2PreQuarter['0'], + $sessionsDuration1PreQuarter['0'], + $percentIncrementSessionDuration0, + $sessionsDurationCurrentQuarter['0'], + ]; + $rows[] = [ + '6-10′', + $sessionsDuration5PreQuarter['5'], + $sessionsDuration4PreQuarter['5'], + $sessionsDuration3PreQuarter['5'], + $sessionsDuration2PreQuarter['5'], + $sessionsDuration1PreQuarter['5'], + $percentIncrementSessionDuration5, + $sessionsDurationCurrentQuarter['5'], + ]; + $rows[] = [ + '11-15′', + $sessionsDuration5PreQuarter['10'], + $sessionsDuration4PreQuarter['10'], + $sessionsDuration3PreQuarter['10'], + $sessionsDuration2PreQuarter['10'], + $sessionsDuration1PreQuarter['10'], + $percentIncrementSessionDuration10, + $sessionsDurationCurrentQuarter['10'], + ]; + $rows[] = [ + '16-30′', + $sessionsDuration5PreQuarter['15'], + $sessionsDuration4PreQuarter['15'], + $sessionsDuration3PreQuarter['15'], + $sessionsDuration2PreQuarter['15'], + $sessionsDuration1PreQuarter['15'], + $percentIncrementSessionDuration15, + $sessionsDurationCurrentQuarter['15'], + ]; + $rows[] = [ + '31-60′', + $sessionsDuration5PreQuarter['30'], + $sessionsDuration4PreQuarter['30'], + $sessionsDuration3PreQuarter['30'], + $sessionsDuration2PreQuarter['30'], + $sessionsDuration1PreQuarter['30'], + $percentIncrementSessionDuration30, + $sessionsDurationCurrentQuarter['30'], + ]; + $rows[] = [ + '60-∞′', + $sessionsDuration5PreQuarter['60'], + $sessionsDuration4PreQuarter['60'], + $sessionsDuration3PreQuarter['60'], + $sessionsDuration2PreQuarter['60'], + $sessionsDuration1PreQuarter['60'], + $percentIncrementSessionDuration60, + $sessionsDurationCurrentQuarter['60'], + ]; + echo Display::table($headers, $rows, []); + echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning'); + break; + case "report_quarterly_courses_and_sessions": + // Make the headers for the tables + $headers = [ + [ + get_lang('List of course codes'), + get_lang('Number of subscribed users').'*', + get_lang('Number of users who finished the course (as defined in gradebook)'), + ], + [ + get_lang('List of course codes and sessions'), + get_lang('Number of subscribed users').'*', + get_lang('Number of users who finished the course (as defined in gradebook)'), + ], + ]; + // Get the data fot the first table + $courses = UserManager::countUsersWhoFinishedCourses(); + //Make the rows for first table + $rows = []; + foreach ($courses as $course => $data) { + $course_url = api_get_path(WEB_CODE_PATH).'course_home/course_home.php?cidReq='.$course; + $rows[] = [ + Display::url($course, $course_url, ['target' => SESSION_LINK_TARGET]), + $data['subscribed'], + $data['finished'], + ]; + } + echo Display::table($headers[0], $rows, []); + //Get the data for the second table (with sessions) + $courses = UserManager::countUsersWhoFinishedCoursesInSessions(); + //Make the rows for second table + $rows = []; + foreach ($courses as $course => $data) { + $rows[] = [ + $course, + $data['subscribed'], + $data['finished'], + ]; + } + echo Display::tag('br', '', ['style' => 'margin-top: 25px;']); + echo Display::table($headers[1], $rows, []); + echo Display::tag('br', '', ['style' => 'margin-top: 25px;']); + echo Display::label(get_lang('*: All users, including inactive, are included'), 'warning'); + break; + case "report_quarterly_total_disk_usage": + $accessUrlId = api_get_current_access_url_id(); + if (api_is_windows_os()) { + $message = get_lang('The space used on disk cannot be measured properly on Windows-based systems.'); + } else { + $dir = api_get_path(SYS_PATH); + $du = exec('du -sh '.$dir, $err); + list($size, $none) = explode("\t", $du); + unset($none); + $limit = 0; + if (isset($_configuration[$accessUrlId]['hosting_limit_disk_space'])) { + $limit = $_configuration[$accessUrlId]['hosting_limit_disk_space']; + } + $message = sprintf(get_lang('Total space used by portal %s limit is %s MB'), $size, $limit); + } + echo Display::tag('H5', $message, ['style' => 'margin-bottom: 25px;']); + break; } diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index fe31007fc9..926dd88f16 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -7534,3 +7534,20 @@ function api_get_permission(string $permissionSlug, array $roles): bool return $permissionService->hasPermission($permissionSlug, $roles); } + +/** + * Calculate the percentage of change between two numbers. + * + * @param int $newValue + * @param int $oldValue + * @return string + */ +function api_calculate_increment_percent(int $newValue, int $oldValue): string +{ + if ($oldValue <= 0) { + $result = " - "; + } else { + $result = ' '.round(100 * (($newValue / $oldValue) - 1), 2).' %'; + } + return $result; +} diff --git a/public/main/inc/lib/internationalization.lib.php b/public/main/inc/lib/internationalization.lib.php index 04a822126c..4424f9add0 100644 --- a/public/main/inc/lib/internationalization.lib.php +++ b/public/main/inc/lib/internationalization.lib.php @@ -2013,3 +2013,58 @@ function api_get_human_date_time($date, $showTime = true, $humanForm = false) } } } + +/** + * Return an array with the start and end dates of a quarter (as in 3 months period). + * If no DateTime is not sent, use the current date. + * + * @param string|null $date (optional) The date or null. + * + * @return array E.G.: ['quarter_start' => '2022-10-11', + * 'quarter_end' => '2022-12-31', + * 'quarter_title' => 'Q4 2022'] + */ +function getQuarterDates(string $date = null): array +{ + if (empty($date)) { + $date = api_get_utc_datetime(); + } + if (strlen($date > 10)) { + $date = substr($date, 0, 10); + } + $month = substr($date, 5, 2); + $year = substr($date, 0, 4); + switch ($month) { + case $month >= 1 && $month <= 3: + $start = "$year-01-01"; + $end = "$year-03-31"; + $quarter = 1; + break; + case $month >= 4 && $month <= 6: + $start = "$year-04-01"; + $end = "$year-06-30"; + $quarter = 2; + break; + case $month >= 7 && $month <= 9: + $start = "$year-07-01"; + $end = "$year-09-30"; + $quarter = 3; + break; + case $month >= 10 && $month <= 12: + $start = "$year-10-01"; + $end = "$year-12-31"; + $quarter = 4; + break; + default: + // Should never happen + $start = "$year-01-01"; + $end = "$year-03-31"; + $quarter = 1; + break; + } + return [ + 'quarter_start' => $start, + 'quarter_end' => $end, + 'quarter_title' => sprintf(get_lang('Q%s %s'), $quarter, $year), + ]; +} diff --git a/public/main/inc/lib/statistics.lib.php b/public/main/inc/lib/statistics.lib.php index 4e31d1c097..cbb3b9bca5 100644 --- a/public/main/inc/lib/statistics.lib.php +++ b/public/main/inc/lib/statistics.lib.php @@ -37,42 +37,44 @@ class Statistics /** * Count courses. * - * @param string $categoryCode Code of a course category. - * Default: count all courses. + * @param string|null $categoryCode Code of a course category. + * Default: count all courses. + * @param string|null $dateFrom dateFrom + * @param string|null $dateUntil dateUntil * * @return int Number of courses counted + * @throws \Doctrine\DBAL\Exception */ - public static function countCourses($categoryCode = null) + public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int { - $course_table = Database::get_main_table(TABLE_MAIN_COURSE); - $tblCourseCategory = Database::get_main_table(TABLE_MAIN_CATEGORY); - $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); + $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); + $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $urlId = api_get_current_access_url_id(); - - $categoryJoin = ''; - $categoryCondition = ''; - - if (!empty($categoryCode)) { - //$categoryJoin = " LEFT JOIN $tblCourseCategory course_category ON course.category_id = course_category.id "; - //$categoryCondition = " course_category.code = '".Database::escape_string($categoryCode)."' "; - } - if (api_is_multiple_url_enabled()) { $sql = "SELECT COUNT(*) AS number - FROM ".$course_table." as c, $access_url_rel_course_table as u - $categoryJoin - WHERE u.c_id = c.id AND access_url_id='".$urlId."'"; + FROM ".$courseTable." AS c, $accessUrlRelCourseTable AS u + WHERE u.c_id = c.id AND $accessUrlRelCourseTable='".$urlId."'"; if (isset($categoryCode)) { - $sql .= " AND $categoryCondition"; + $sql .= " AND category_code = '".Database::escape_string($categoryCode)."'"; } } else { $sql = "SELECT COUNT(*) AS number - FROM $course_table $categoryJoin"; + FROM $courseTable AS c + WHERE 1 = 1"; if (isset($categoryCode)) { - $sql .= " WHERE $categoryCondition"; + $sql .= " WHERE c.category_code = '".Database::escape_string($categoryCode)."'"; } } + if (!empty($dateFrom)) { + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $sql .= " AND c.creation_date >= '$dateFrom' "; + } + if (!empty($dateUntil)) { + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + $sql .= " AND c.creation_date <= '$dateUntil' "; + } + $res = Database::query($sql); $obj = Database::fetch_object($res); @@ -82,30 +84,52 @@ class Statistics /** * Count courses by visibility. * - * @param int $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses + * @param array|null $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses + * @param string|null $dateFrom dateFrom + * @param string|null $dateUntil dateUntil * * @return int Number of courses counted + * @throws \Doctrine\DBAL\Exception */ - public static function countCoursesByVisibility($visibility = null) + public static function countCoursesByVisibility( + array $visibility = null, + string $dateFrom = null, + string $dateUntil = null + ): int { - if (!isset($visibility)) { + if (empty($visibility)) { return 0; + } else { + $visibilityString = ''; + $auxArrayVisibility = []; + if (!is_array($visibility)) { + $visibility = [$visibility]; + } + foreach ($visibility as $item) { + $auxArrayVisibility[] = (int) $item; + } + $visibilityString = implode(',', $auxArrayVisibility); } - $course_table = Database::get_main_table(TABLE_MAIN_COURSE); - $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); + $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); + $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $urlId = api_get_current_access_url_id(); if (api_is_multiple_url_enabled()) { $sql = "SELECT COUNT(*) AS number - FROM $course_table as c, $access_url_rel_course_table as u - WHERE u.c_id = c.id AND access_url_id='".$urlId."'"; - if (isset($visibility)) { - $sql .= " AND visibility = ".intval($visibility); - } + FROM $courseTable AS c, $accessUrlRelCourseTable AS u + WHERE u.c_id = c.id AND u.access_url_id='".$urlId."'"; } else { - $sql = "SELECT COUNT(*) AS number FROM $course_table "; - if (isset($visibility)) { - $sql .= " WHERE visibility = ".intval($visibility); - } + $sql = "SELECT COUNT(*) AS number + FROM $courseTable AS c + WHERE 1 = 1"; + } + $sql .= " AND visibility IN ($visibilityString) "; + if (!empty($dateFrom)) { + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $sql .= " AND c.creation_date >= '$dateFrom' "; + } + if (!empty($dateUntil)) { + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + $sql .= " AND c.creation_date <= '$dateUntil' "; } $res = Database::query($sql); $obj = Database::fetch_object($res); @@ -1516,7 +1540,7 @@ class Statistics * * @return array */ - private static function getLoginsByDate($startDate, $endDate) + public static function getLoginsByDate(string $startDate, string $endDate): array { $startDate = api_get_utc_datetime("$startDate 00:00:00"); $endDate = api_get_utc_datetime("$endDate 23:59:59"); @@ -1665,4 +1689,88 @@ class Statistics return $groupedData; } + + /** + * Return de number of certificates generated. + * This function is resource intensive. + * @throws \Doctrine\DBAL\Exception + * @throws Exception + */ + public static function countCertificatesByQuarter(string $dateFrom = null, string $dateUntil = null): int + { + $tableGradebookCertificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE); + $condition = ""; + if (!empty($dateFrom) && !empty($dateUntil)) { + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + $condition = "WHERE (created_at BETWEEN '$dateFrom' AND '$dateUntil')"; + } elseif (!empty($dateFrom)) { + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $condition = "WHERE created_at >= '$dateFrom'"; + } elseif (!empty($dateUntil)) { + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + $condition = "WHERE created_at <= '$dateUntil'"; + } + $sql = " + SELECT count(*) AS count + FROM $tableGradebookCertificate + $condition + "; + $response = Database::query($sql); + $obj = Database::fetch_object($response); + return $obj->count; + } + + /** + * Get the number of logins by dates. + * This function is resource intensive. + * @throws Exception + */ + public static function getSessionsByDuration(string $dateFrom, string $dateUntil): array + { + $results = [ + '0' => 0, + '5' => 0, + '10' => 0, + '15' => 0, + '30' => 0, + '60' => 0, + ]; + if (!empty($dateFrom) && !empty($dateUntil)) { + $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN); + $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); + $urlId = api_get_current_access_url_id(); + $tableUrl = ''; + $whereUrl = ''; + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + if (api_is_multiple_url_enabled()) { + $tableUrl = ", $accessUrlRelUserTable"; + $whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId"; + } + $sql = "SELECT login_id, TIMESTAMPDIFF(SECOND, login_date, logout_date) AS duration + FROM $table $tableUrl + WHERE login_date >= '$dateFrom' + AND logout_date <= '$dateUntil' + $whereUrl + "; + $res = Database::query($sql); + while ($session = Database::fetch_array($res)) { + if ($session['duration'] > 3600) { + $results['60']++; + } elseif ($session['duration'] > 1800) { + $results['30']++; + } elseif ($session['duration'] > 900) { + $results['15']++; + } elseif ($session['duration'] > 600) { + $results['10']++; + } elseif ($session['duration'] > 300) { + $results['5']++; + } else { + $results['0']++; + } + } + } + return $results; + } } diff --git a/public/main/inc/lib/tracking.lib.php b/public/main/inc/lib/tracking.lib.php index 8373faa687..4c29d97603 100644 --- a/public/main/inc/lib/tracking.lib.php +++ b/public/main/inc/lib/tracking.lib.php @@ -8134,4 +8134,40 @@ class Tracking return $exeDate; } + /** + * Return the total time spent in courses (no the total in platform). + * + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public static function getTotalTimeSpentInCourses( + string $dateFrom = '', + string $dateUntil = '' + ): int { + $tableTrackLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS); + $tableUrlRelUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); + $tableUrl = null; + $urlCondition = null; + $conditionTime = null; + if (api_is_multiple_url_enabled()) { + $accessUrlId = api_get_current_access_url_id(); + $tableUrl = ", ".$tableUrlRelUser." as url_users"; + $urlCondition = " AND u.user_id = url_users.user_id AND access_url_id = $accessUrlId"; + } + if (!empty($dateFrom) && !empty($dateUntil)) { + $dateFrom = Database::escape_string($dateFrom); + $dateUntil = Database::escape_string($dateUntil); + $conditionTime = " (login_course_date >= '$dateFrom' AND logout_course_date <= '$dateUntil' ) "; + } + $sql = "SELECT SUM(TIMESTAMPDIFF(HOUR, login_course_date, logout_course_date)) diff + FROM $tableTrackLogin u $tableUrl + WHERE $conditionTime $urlCondition"; + $rs = Database::query($sql); + $row = Database::fetch_array($rs, 'ASSOC'); + $diff = $row['diff']; + if ($diff >= 0 and !empty($diff)) { + return $diff; + } + return 0; + } } diff --git a/public/main/inc/lib/usermanager.lib.php b/public/main/inc/lib/usermanager.lib.php index a25621e931..0b126ae8ec 100644 --- a/public/main/inc/lib/usermanager.lib.php +++ b/public/main/inc/lib/usermanager.lib.php @@ -3368,27 +3368,33 @@ class UserManager /** * Get the total count of users. * - * @param int $status Status of users to be counted - * @param int $access_url_id Access URL ID (optional) - * @param int $active + * @param ?int $status Status of users to be counted + * @param ?int $access_url_id Access URL ID (optional) + * @param ?int $active * * @return mixed Number of users or false on error + * @throws \Doctrine\DBAL\Exception */ - public static function get_number_of_users($status = 0, $access_url_id = 1, $active = null) - { - $t_u = Database::get_main_table(TABLE_MAIN_USER); - $t_a = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); + public static function get_number_of_users( + ?int $status = 0, + ?int $access_url_id = 1, + ?int $active = null, + ?string $dateFrom = null, + ?string $dateUntil = null + ): mixed { + $tableUser = Database::get_main_table(TABLE_MAIN_USER); + $tableAccessUrlRelUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); if (api_is_multiple_url_enabled()) { $sql = "SELECT count(u.id) - FROM $t_u u - INNER JOIN $t_a url_user + FROM $tableUser u + INNER JOIN $tableAccessUrlRelUser url_user ON (u.id = url_user.user_id) WHERE url_user.access_url_id = $access_url_id "; } else { $sql = "SELECT count(u.id) - FROM $t_u u + FROM $tableUser u WHERE 1 = 1 "; } @@ -3397,11 +3403,20 @@ class UserManager $sql .= " AND u.status = $status "; } - if (null !== $active) { + if (isset($active)) { $active = (int) $active; $sql .= " AND u.active = $active "; } + if (!empty($dateFrom)) { + $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); + $sql .= " AND u.registration_date >= '$dateFrom' "; + } + if (!empty($dateUntil)) { + $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); + $sql .= " AND u.registration_date <= '$dateUntil' "; + } + $res = Database::query($sql); if (1 === Database::num_rows($res)) { return (int) Database::result($res, 0, 0); @@ -6066,4 +6081,106 @@ SQL; return $url; } + + /** + * Count users in courses and if they have certificate. + * This function is resource intensive. + * + * @return array + * @throws Exception + * @throws \Doctrine\DBAL\Exception + */ + public static function countUsersWhoFinishedCourses() + { + $courses = []; + $currentAccessUrlId = api_get_current_access_url_id(); + $sql = "SELECT course.code, cru.user_id + FROM course_rel_user cru + JOIN course ON cru.c_id = course.id + JOIN access_url_rel_user auru on cru.user_id = auru.user_id + JOIN access_url_rel_course ON course.id = access_url_rel_course.c_id + WHERE access_url_rel_course.access_url_id = $currentAccessUrlId + ORDER BY course.code + "; + $res = Database::query($sql); + if (Database::num_rows($res) > 0) { + while ($row = Database::fetch_array($res)) { + if (!isset($courses[$row['code']])) { + $courses[$row['code']] = [ + 'subscribed' => 0, + 'finished' => 0, + ]; + } + $courses[$row['code']]['subscribed']++; + $entityManager = Database::getManager(); + $repository = $entityManager->getRepository('ChamiloCoreBundle:GradebookCategory'); + //todo check when have more than 1 gradebook + /** @var \Chamilo\CoreBundle\Entity\GradebookCategory $gradebook */ + $gradebook = $repository->findOneBy(['courseCode' => $row['code']]); + if (!empty($gradebook)) { + $finished = 0; + $gb = Category::createCategoryObjectFromEntity($gradebook); + $finished = $gb->is_certificate_available($row['user_id']); + if (!empty($finished)) { + $courses[$row['code']]['finished']++; + } + } + } + } + return $courses; + } + + /** + * Count users in sessions and if they have certificate. + * This function is resource intensive. + * + * @return array + * @throws Exception + * @throws \Doctrine\DBAL\Exception + */ + public static function countUsersWhoFinishedCoursesInSessions() + { + $coursesInSessions = []; + $currentAccessUrlId = api_get_current_access_url_id(); + $sql = "SELECT course.code, srcru.session_id, srcru.user_id, session.title + FROM session_rel_course_rel_user srcru + JOIN course ON srcru.c_id = course.id + JOIN access_url_rel_session aurs on srcru.session_id = aurs.session_id + JOIN session ON srcru.session_id = session.id + WHERE aurs.access_url_id = $currentAccessUrlId + ORDER BY course.code, session.title + "; + $res = Database::query($sql); + if (Database::num_rows($res) > 0) { + while ($row = Database::fetch_array($res)) { + $index = $row['code'].' ('.$row['title'].')'; + if (!isset($coursesInSessions[$index])) { + $coursesInSessions[$index] = [ + 'subscribed' => 0, + 'finished' => 0, + ]; + } + $coursesInSessions[$index]['subscribed']++; + $entityManager = Database::getManager(); + $repository = $entityManager->getRepository('ChamiloCoreBundle:GradebookCategory'); + /** @var \Chamilo\CoreBundle\Entity\GradebookCategory $gradebook */ + $gradebook = $repository->findOneBy( + [ + 'courseCode' => $row['code'], + 'sessionId' => $row['session_id'], + ] + ); + if (!empty($gradebook)) { + $finished = 0; + $gb = Category::createCategoryObjectFromEntity($gradebook); + $finished = $gb->is_certificate_available($row['user_id']); + if (!empty($finished)) { + $coursesInSessions[$index]['finished']++; + } + } + } + } + return $coursesInSessions; + } + } diff --git a/public/plugin/dashboard/block_global_info/block_global_info.class.php b/public/plugin/dashboard/block_global_info/block_global_info.class.php index b66e3408a2..f874722542 100644 --- a/public/plugin/dashboard/block_global_info/block_global_info.class.php +++ b/public/plugin/dashboard/block_global_info/block_global_info.class.php @@ -122,11 +122,11 @@ class BlockGlobalInfo extends Block [get_lang('Number of active users'), ''.Statistics::countUsers(null, null, null, true).''], // Check number of courses [get_lang('Total number of courses'), ''.Statistics::countCourses().''], - [get_lang('Number of public courses'), ''.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_OPEN_WORLD).''], - [get_lang('Number of open courses'), ''.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_OPEN_PLATFORM).''], - [get_lang('Number of private courses'), ''.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_REGISTERED).''], - [get_lang('Number of closed courses'), ''.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_CLOSED).''], - [get_lang('Number of hidden courses'), ''.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_HIDDEN).''], + [get_lang('Number of public courses'), ''.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_OPEN_WORLD]).''], + [get_lang('Number of open courses'), ''.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_OPEN_PLATFORM]).''], + [get_lang('Number of private courses'), ''.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_REGISTERED]).''], + [get_lang('Number of closed courses'), ''.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_CLOSED]).''], + [get_lang('Number of hidden courses'), ''.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_HIDDEN]).''], ]; } } From 29359940175debcdb438bb6443182de2af5e3caf Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Thu, 23 Jan 2025 15:02:26 +0100 Subject: [PATCH 10/33] Internal: Convert deprecated api_is_multiple_url_enabled() to AccessUrlHelper::isMultiple() --- public/main/inc/lib/statistics.lib.php | 37 +++++++++++++------------- public/main/inc/lib/tracking.lib.php | 21 ++++++++------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/public/main/inc/lib/statistics.lib.php b/public/main/inc/lib/statistics.lib.php index cbb3b9bca5..d7c8589a5c 100644 --- a/public/main/inc/lib/statistics.lib.php +++ b/public/main/inc/lib/statistics.lib.php @@ -5,6 +5,7 @@ use Chamilo\CoreBundle\Component\Utils\ChamiloApi; use Chamilo\CoreBundle\Entity\MessageRelUser; use Chamilo\CoreBundle\Entity\UserRelUser; use Chamilo\CoreBundle\Component\Utils\ActionIcon; +use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper; /** * This class provides some functions for statistics. @@ -50,7 +51,7 @@ class Statistics $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT COUNT(*) AS number FROM ".$courseTable." AS c, $accessUrlRelCourseTable AS u WHERE u.c_id = c.id AND $accessUrlRelCourseTable='".$urlId."'"; @@ -113,7 +114,7 @@ class Statistics $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT COUNT(*) AS number FROM $courseTable AS c, $accessUrlRelCourseTable AS u WHERE u.c_id = c.id AND u.access_url_id='".$urlId."'"; @@ -173,7 +174,7 @@ class Statistics $where = implode(' AND ', $conditions); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT COUNT(DISTINCT(u.id)) AS number FROM $user_table as u INNER JOIN $access_url_rel_user_table as url ON u.id = url.user_id @@ -227,7 +228,7 @@ class Statistics $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT DISTINCT(t.c_id) FROM $table t , $access_url_rel_course_table a WHERE t.c_id = a.c_id AND @@ -256,7 +257,7 @@ class Statistics $table_user = Database::get_main_table(TABLE_MAIN_USER); $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT count(default_id) AS total_number_of_items FROM $track_e_default, $table_user user, $access_url_rel_user_table url WHERE user.active <> ".USER_SOFT_DELETED." AND @@ -322,7 +323,7 @@ class Statistics $direction = 'DESC'; } - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT default_event_type as col0, default_value_type as col1, @@ -546,7 +547,7 @@ class Statistics $where_url = null; $now = api_get_utc_datetime(); $where_url_last = ' WHERE login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)'; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $table_url = ", $access_url_rel_user_table"; $where_url = " WHERE login_user_id=user_id AND access_url_id='".$urlId."'"; $where_url_last = ' AND login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)'; @@ -646,7 +647,7 @@ class Statistics $urlId = api_get_current_access_url_id(); $table_url = ''; $where_url = ''; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $table_url = ", $access_url_rel_user_table"; $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'"; } @@ -736,7 +737,7 @@ class Statistics $urlId = api_get_current_access_url_id(); $table_url = ''; $where_url = ''; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $table_url = ", $access_url_rel_user_table"; $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'"; } @@ -802,7 +803,7 @@ class Statistics foreach ($tools as $tool) { $tool_names[$tool] = get_lang(ucfirst($tool), ''); } - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT access_tool, count( access_id ) AS number_of_logins FROM $table t , $access_url_rel_course_table a WHERE @@ -851,7 +852,7 @@ class Statistics $table = Database::get_main_table(TABLE_MAIN_COURSE); $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT course_language, count( c.code ) AS number_of_courses FROM $table as c, $access_url_rel_course_table as u WHERE u.c_id = c.id AND access_url_id='".$urlId."' @@ -882,7 +883,7 @@ class Statistics $url_condition = null; $url_condition2 = null; $table = null; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $url_condition = ", $access_url_rel_user_table as url WHERE url.user_id=u.id AND access_url_id='".$urlId."'"; $url_condition2 = " AND url.user_id=u.id AND access_url_id='".$urlId."'"; $table = ", $access_url_rel_user_table as url "; @@ -995,7 +996,7 @@ class Statistics $values = $form->exportValues(); $date_diff = $values['date_diff']; $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT * FROM $table t , $access_url_rel_course_table a WHERE c_id = a.c_id AND @@ -1074,7 +1075,7 @@ class Statistics break; } - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message FROM $messageTable m INNER JOIN $messageRelUserTable mru ON $joinCondition @@ -1119,7 +1120,7 @@ class Statistics $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend FROM $access_url_rel_user_table as url, $user_friend_table uf LEFT JOIN $user_table u @@ -1159,7 +1160,7 @@ class Statistics $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); $urlId = api_get_current_access_url_id(); $total = self::countUsers(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $table_url = ", $access_url_rel_user_table"; $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'"; } else { @@ -1554,7 +1555,7 @@ class Statistics $urlJoin = ''; $urlWhere = ''; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id"; @@ -1744,7 +1745,7 @@ class Statistics $whereUrl = ''; $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00"); $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59"); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $tableUrl = ", $accessUrlRelUserTable"; $whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId"; } diff --git a/public/main/inc/lib/tracking.lib.php b/public/main/inc/lib/tracking.lib.php index 4c29d97603..5649073551 100644 --- a/public/main/inc/lib/tracking.lib.php +++ b/public/main/inc/lib/tracking.lib.php @@ -20,6 +20,7 @@ use CpChart\Image as pImage; use ExtraField as ExtraFieldModel; use Chamilo\CoreBundle\Component\Utils\ActionIcon; use Chamilo\CoreBundle\Component\Utils\StateIcon; +use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper; /** * Class Tracking. @@ -1745,7 +1746,7 @@ class Tracking $url_condition = null; $tbl_url_rel_user = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER); $url_table = null; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $access_url_id = api_get_current_access_url_id(); $url_table = ", $tbl_url_rel_user as url_users"; $url_condition = " AND u.login_user_id = url_users.user_id AND access_url_id='$access_url_id'"; @@ -1827,7 +1828,7 @@ class Tracking $url_table = null; $url_condition = null; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $access_url_id = api_get_current_access_url_id(); $url_table = ", ".$tbl_url_rel_user." as url_users"; $url_condition = " AND u.login_user_id = url_users.user_id AND access_url_id='$access_url_id'"; @@ -3599,7 +3600,7 @@ class Tracking $tbl_session_user = Database::get_main_table(TABLE_MAIN_SESSION_USER); $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION); - $accessUrlEnabled = api_is_multiple_url_enabled(); + $accessUrlEnabled = AccessUrlHelper::isMultiple(); $access_url_id = $accessUrlEnabled ? api_get_current_access_url_id() : -1; $students = []; @@ -3754,7 +3755,7 @@ class Tracking ON (c.id = sc.c_id) WHERE sc.user_id = '.$coach_id.' AND sc.status = '.SessionEntity::COURSE_COACH; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $access_url_id = api_get_current_access_url_id(); if (-1 != $access_url_id) { $sql = 'SELECT DISTINCT c.code @@ -3792,7 +3793,7 @@ class Tracking INNER JOIN $tbl_course as course ON course.id = session_course.c_id"; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $tbl_course_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE); $access_url_id = api_get_current_access_url_id(); if (-1 != $access_url_id) { @@ -3815,11 +3816,11 @@ class Tracking if (!empty($sessionId)) { $sql .= ' WHERE session_course.session_id='.$sessionId; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql .= ' AND access_url_id = '.$access_url_id; } } else { - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql .= ' WHERE access_url_id = '.$access_url_id; } } @@ -4596,7 +4597,7 @@ class Tracking $session_id = (int) $session_id; $urlId = api_get_current_access_url_id(); - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT c.id, c.code, title FROM $tbl_course_user cu INNER JOIN $tbl_course c @@ -4643,7 +4644,7 @@ class Tracking } // Get the list of sessions where the user is subscribed as student - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $sql = "SELECT DISTINCT c.code, s.id as session_id, s.title FROM $tbl_session_course_user cu INNER JOIN $tbl_access_rel_session a @@ -8149,7 +8150,7 @@ class Tracking $tableUrl = null; $urlCondition = null; $conditionTime = null; - if (api_is_multiple_url_enabled()) { + if (AccessUrlHelper::isMultiple()) { $accessUrlId = api_get_current_access_url_id(); $tableUrl = ", ".$tableUrlRelUser." as url_users"; $urlCondition = " AND u.user_id = url_users.user_id AND access_url_id = $accessUrlId"; From 5d84393c3842709da4cfb289bd4a8934b5c15740 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:08:56 -0500 Subject: [PATCH 11/33] Fix question list from admin page - refs BT#22276 --- public/main/admin/questions.php | 162 +++++++++++++++--- .../default/admin/questions.html.twig | 40 +++-- 2 files changed, 162 insertions(+), 40 deletions(-) diff --git a/public/main/admin/questions.php b/public/main/admin/questions.php index 9bc0dc4ba0..ba4ba449e8 100644 --- a/public/main/admin/questions.php +++ b/public/main/admin/questions.php @@ -20,14 +20,86 @@ Session::erase('objExercise'); Session::erase('objQuestion'); Session::erase('objAnswer'); -$interbreadcrumb[] = ['url' => '/admin', 'name' => get_lang('Administration')]; +$interbreadcrumb[] = ['url' => Container::getRouter()->generate('admin'), 'name' => get_lang('Administration')]; +$action = $_REQUEST['action'] ?? ''; +$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : ''; +$description = $_REQUEST['description'] ?? ''; +$title = $_REQUEST['title'] ?? ''; +$page = !empty($_GET['page']) ? (int) $_GET['page'] : 1; + +// Prepare lists for form +// Courses list +$selectedCourse = isset($_GET['selected_course']) ? (int) $_GET['selected_course'] : null; +$courseList = CourseManager::get_courses_list(0, 0, 'title'); +$courseSelectionList = ['-1' => get_lang('Select')]; + +foreach ($courseList as $item) { + $course = api_get_course_entity($item['real_id']); + $courseSelectionList[$course->getId()] = ''; + + if ($course->getId() == api_get_course_int_id()) { + $courseSelectionList[$course->getId()] = '>    '; + } + + $courseSelectionList[$course->getId()] .= $course->getTitle(); +} + +// Difficulty list (only from 0 to 5) +$questionLevel = isset($_REQUEST['question_level']) ? (int) $_REQUEST['question_level'] : -1; +$levels = [ + -1 => get_lang('All'), + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, +]; + +// Answer type +$answerType = isset($_REQUEST['answer_type']) ? (int) $_REQUEST['answer_type'] : null; +$questionList = Question::getQuestionTypeList(); +$questionTypesList = []; +$questionTypesList['-1'] = get_lang('All'); + +foreach ($questionList as $key => $item) { + $questionTypesList[$key] = get_lang($item[1]); +} $form = new FormValidator('admin_questions', 'get'); $form->addHeader(get_lang('Questions')); $form->addText('id', get_lang('Id'), false); $form->addText('title', get_lang('Title'), false); $form->addText('description', get_lang('Description'), false); +$form + ->addSelect( + 'selected_course', + [get_lang('Course'), get_lang('Course in which the question was initially created.')], + $courseSelectionList, + ['id' => 'selected_course'] + ) + ->setSelected($selectedCourse) +; +$form + ->addSelect( + 'question_level', + get_lang('Difficulty'), + $levels, + ['id' => 'question_level'] + ) + ->setSelected($questionLevel) +; +$form + ->addSelect( + 'answer_type', + get_lang('Answer type'), + $questionTypesList, + ['id' => 'answer_type'] + ) + ->setSelected($answerType) +; $form->addHidden('form_sent', 1); +$form->addHidden('course_id_changed', '0'); $form->addButtonSearch(get_lang('Search')); $questions = []; @@ -37,12 +109,19 @@ $length = 20; $questionCount = 0; $start = 0; $end = 0; +$pdfContent = ''; + +$params = [ + 'id' => $id, + 'title' => Security::remove_XSS($title), + 'description' => Security::remove_XSS($description), + 'selected_course' => $selectedCourse, + 'question_level' => $questionLevel, + 'answer_type' => $answerType, +]; if ($formSent) { - $id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : ''; - $description = isset($_REQUEST['description']) ? $_REQUEST['description'] : ''; - $title = isset($_REQUEST['title']) ? $_REQUEST['title'] : ''; - $page = isset($_GET['page']) && !empty($_GET['page']) ? (int) $_GET['page'] : 1; + $params['form_sent'] = 1; $em = Database::getManager(); $repo = $em->getRepository(CQuizQuestion::class); @@ -61,23 +140,27 @@ if ($formSent) { $criteria->orWhere($criteria->expr()->contains('question', "%$title%")); } - $questions = $repo->matching($criteria); +// if (-1 !== $selectedCourse) { +// $criteria->andWhere($criteria->expr()->eq('cId', $selectedCourse)); +// } - if (empty($id)) { - $id = ''; + if (-1 !== $questionLevel) { + $criteria->andWhere($criteria->expr()->eq('level', $questionLevel)); + } + if (-1 !== $answerType) { + $criteria->andWhere($criteria->expr()->eq('type', $answerType)); } - $params = [ - 'id' => $id, - 'title' => Security::remove_XSS($title), - 'description' => Security::remove_XSS($description), - 'form_sent' => 1, - ]; - $url = api_get_self().'?'.http_build_query($params); - $form->setDefaults($params); + $questions = $repo->matching($criteria); + $url = api_get_self().'?'.http_build_query($params); + $form->setDefaults($params); $questionCount = count($questions); + if ('export_pdf' === $action) { + $length = $questionCount; + } + $paginator = new Paginator(Container::$container->get('event_dispatcher')); $pagination = $paginator->paginate($questions, $page, $length); $pagination->setItemNumberPerPage($length); @@ -142,6 +225,14 @@ if ($formSent) { ); $question->questionData = ob_get_contents(); + if ('export_pdf' === $action) { + $pdfContent .= '#'.$question->getIid().'. '.$question->getQuestion().'
'; + $pdfContent .= '('.$questionTypesList[$question->getType()].') ['.get_lang('Source').': '.$courseCode.']
'; + $pdfContent .= $question->getDescription().'
'; + $pdfContent .= $question->questionData; + continue; + } + $deleteUrl = $url.'&'.http_build_query([ 'courseId' => $question->getCId(), 'questionId' => $question->getId(), @@ -222,11 +313,20 @@ if ($formSent) { $formContent = $form->returnForm(); -$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : ''; switch ($action) { + case 'export_pdf': + $pdfContent = Security::remove_XSS($pdfContent); + $pdfParams = [ + 'filename' => 'questions-export-'.api_get_local_time(), + 'pdf_date' => api_get_local_time(), + 'orientation' => 'P', + ]; + $pdf = new PDF('A4', $pdfParams['orientation'], $pdfParams); + $pdf->html_to_pdf_with_template($pdfContent, false, false, true); + exit; case 'delete': - $questionId = isset($_REQUEST['questionId']) ? $_REQUEST['questionId'] : ''; - $courseId = isset($_REQUEST['courseId']) ? $_REQUEST['courseId'] : ''; + $questionId = $_REQUEST['questionId'] ?? ''; + $courseId = $_REQUEST['courseId'] ?? ''; $courseInfo = api_get_course_info_by_id($courseId); if (!empty($courseInfo)) { @@ -245,16 +345,36 @@ switch ($action) { header("Location: $url"); exit; - break; } +$actionsLeft = Display::url( + Display::return_icon('back.png', get_lang('Administration'), [], ICON_SIZE_MEDIUM), + Container::getRouter()->generate('admin'), +); + +$exportUrl = Container::getRouter()->generate( + 'legacy_main', + ['name' => 'admin/questions.php', 'action' => 'export_pdf', ...$params] +); + +$actionsRight = Display::url( + Display::return_icon('pdf.png', get_lang('Export to PDF'), [], ICON_SIZE_MEDIUM), + $exportUrl +); + +$toolbar = Display::toolbarAction( + 'toolbar-admin-questions', + [$actionsLeft, $actionsRight] +); + $tpl = new Template(get_lang('Questions')); $tpl->assign('form', $formContent); +$tpl->assign('toolbar', $toolbar); $tpl->assign('pagination', $pagination); $tpl->assign('pagination_length', $length); $tpl->assign('start', $start); $tpl->assign('end', $end); $tpl->assign('question_count', $questionCount); -$layout = $tpl->get_template('admin/questions.tpl'); +$layout = $tpl->get_template('admin/questions.html.twig'); $tpl->display($layout); diff --git a/public/main/template/default/admin/questions.html.twig b/public/main/template/default/admin/questions.html.twig index 59327da3ed..1ff78d8d35 100644 --- a/public/main/template/default/admin/questions.html.twig +++ b/public/main/template/default/admin/questions.html.twig @@ -2,24 +2,26 @@ {% import '@ChamiloCore/Macros/box.html.twig' as display %} {% block content %} - {{ toolbar }} - {{ form }} -{# {% for question in pagination %}#} - {% for i in start..end %} - {% if pagination[i] is defined %} - {% set question = pagination[i] %} - {{ display.collapse( - question.iid, - '#' ~ question.courseCode ~'-'~ question.iid ~ ' - ' ~ question.question, - question.questionData, - false, - false - ) - }} - {% endif %} - {% endfor %} + {% autoescape false %} + {{ toolbar }} + {{ form }} + {# {% for question in pagination %}#} + {% for i in start..end %} + {% if pagination[i] is defined %} + {% set question = pagination[i] %} + {{ display.collapse( + question.iid, + '#' ~ question.courseCode ~'-'~ question.iid ~ ' - ' ~ question.question, + question.questionData, + false, + false + ) + }} + {% endif %} + {% endfor %} - {% if question_count > pagination_length %} - {{ pagination }} - {% endif %} + {% if question_count > pagination_length %} + {{ pagination }} + {% endif %} + {% endautoescape %} {% endblock %} From 24ebed69f35318c34c91b3c3ade7dfe801038636 Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Thu, 23 Jan 2025 15:28:58 +0100 Subject: [PATCH 12/33] Internal: Extend timeout for composer execution (taking longer than the default 300s timeout in some cases) --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1d4caaf729..1ed2adea4c 100755 --- a/composer.json +++ b/composer.json @@ -181,7 +181,8 @@ "symfony/flex": true, "dealerdirect/phpcodesniffer-composer-installer": true, "symfony/runtime": true - } + }, + "process-timeout": 900 }, "require-dev": { "behat/behat": "^3.10", From 706a656eb542ba1ceb5aabb72054b97ea86716be Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:52:49 -0500 Subject: [PATCH 13/33] Internal: Add missing DB port for doctrine connection Fix #6025 --- config/packages/doctrine.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index bee917bb2a..c6f7cae6c1 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -5,6 +5,7 @@ doctrine: user: '%env(DATABASE_USER)%' password: '%env(DATABASE_PASSWORD)%' host: '%env(DATABASE_HOST)%' + port: '%env(DATABASE_PORT)%' driver: 'pdo_mysql' charset: utf8mb4 options: From a21a5c23cecba5d556cb1ef1834968c25b17ed65 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:01:23 -0500 Subject: [PATCH 14/33] Display: Improvements to the installation process forms --- assets/vue/AppInstaller.vue | 29 +++++------ assets/vue/components/installer/Step1.vue | 59 ++++++++++------------ assets/vue/components/installer/Step2.vue | 6 +-- assets/vue/components/installer/Step3.vue | 61 ++++++++++------------- assets/vue/components/installer/Step4.vue | 6 +-- assets/vue/components/installer/Step5.vue | 7 +-- assets/vue/components/installer/Step6.vue | 7 +-- assets/vue/components/installer/Step7.vue | 35 ++++++------- public/main/install/index.php | 11 ++-- 9 files changed, 105 insertions(+), 116 deletions(-) diff --git a/assets/vue/AppInstaller.vue b/assets/vue/AppInstaller.vue index 8ac7973caf..1058f8cea8 100644 --- a/assets/vue/AppInstaller.vue +++ b/assets/vue/AppInstaller.vue @@ -20,18 +20,17 @@ {{ stepTitle }} - @@ -39,19 +38,19 @@

-

+ -
-
- -
- -
+ -

- {{ t("An update is available. Click the button below to proceed with the update.") }} -

+

{{ installerData.checkMigrationStatus.message }}

Current Migration: {{ installerData.checkMigrationStatus.current_migration }} @@ -63,12 +56,12 @@


-