first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Listing of the course administration pages for this course.
*
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once("../config.php");
$courseid = required_param('courseid', PARAM_INT);
$PAGE->set_url('/course/admin.php', array('courseid'=>$courseid));
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
require_login($course);
$context = context_course::instance($course->id);
$PAGE->set_pagelayout('incourse');
if ($courseid == $SITE->id) {
$title = get_string('frontpagesettings');
$node = $PAGE->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
$PAGE->set_primary_active_tab('home');
} else {
$title = get_string('courseadministration');
$node = $PAGE->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
}
$PAGE->set_title($title);
$PAGE->set_heading($course->fullname);
$PAGE->navbar->add($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
if ($node) {
echo $OUTPUT->render_from_template('core/settings_link_page', ['node' => $node]);
}
echo $OUTPUT->footer();
+158
View File
@@ -0,0 +1,158 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Performs course category management ajax actions.
*
* Please note functions may throw exceptions, please ensure your JS handles them as well as the outcome objects.
*
* @package core_course
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require_once('../../config.php');
require_once($CFG->dirroot.'/course/lib.php');
$action = required_param('action', PARAM_ALPHA);
require_sesskey(); // Gotta have the sesskey.
require_login(); // Gotta be logged in (of course).
$PAGE->set_context(context_system::instance());
// Prepare an outcome object. We always use this.
$outcome = new stdClass;
$outcome->error = false;
$outcome->outcome = false;
echo $OUTPUT->header();
switch ($action) {
case 'movecourseup' :
$courseid = required_param('courseid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_course_change_sortorder_up_one_by_record($courseid);
break;
case 'movecoursedown' :
$courseid = required_param('courseid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_course_change_sortorder_down_one_by_record($courseid);
break;
case 'movecourseintocategory':
$courseid = required_param('courseid', PARAM_INT);
$categoryid = required_param('categoryid', PARAM_INT);
$course = get_course($courseid);
$oldcategory = core_course_category::get($course->category);
$category = core_course_category::get($categoryid);
$outcome->outcome = \core_course\management\helper::move_courses_into_category($category, $courseid);
$perpage = (int)get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage);
$totalcourses = $oldcategory->get_courses_count();
$totalpages = ceil($totalcourses / $perpage);
if ($totalpages == 0) {
$str = get_string('nocoursesyet');
} else if ($totalpages == 1) {
$str = get_string('showingacourses', 'moodle', $totalcourses);
} else {
$a = new stdClass;
$a->start = ($page * $perpage) + 1;
$a->end = min((($page + 1) * $perpage), $totalcourses);
$a->total = $totalcourses;
$str = get_string('showingxofycourses', 'moodle', $a);
}
$outcome->totalcatcourses = $category->get_courses_count();
$outcome->fromcatcoursecount = $totalcourses;
$outcome->paginationtotals = $str;
break;
case 'movecourseafter' :
$courseid = required_param('courseid', PARAM_INT);
$moveaftercourseid = required_param('moveafter', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_course_change_sortorder_after_course(
$courseid, $moveaftercourseid);
break;
case 'hidecourse' :
$courseid = required_param('courseid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_course_hide_by_record($courseid);
break;
case 'showcourse' :
$courseid = required_param('courseid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_course_show_by_record($courseid);
break;
case 'movecategoryup' :
$categoryid = required_param('categoryid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_category_change_sortorder_up_one_by_id($categoryid);
break;
case 'movecategorydown' :
$categoryid = required_param('categoryid', PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_category_change_sortorder_down_one_by_id($categoryid);
break;
case 'hidecategory' :
$categoryid = required_param('categoryid', PARAM_INT);
$selectedcategoryid = optional_param('selectedcategory', null, PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_category_hide_by_id($categoryid);
$outcome->categoryvisibility = \core_course\management\helper::get_category_children_visibility($categoryid);
$outcome->coursevisibility = \core_course\management\helper::get_category_courses_visibility($categoryid);
if ($selectedcategoryid !== null) {
$outcome->coursevisibility = array_merge(
$outcome->coursevisibility,
\core_course\management\helper::get_category_courses_visibility($selectedcategoryid)
);
}
break;
case 'showcategory' :
$categoryid = required_param('categoryid', PARAM_INT);
$selectedcategoryid = optional_param('selectedcategory', null, PARAM_INT);
$outcome->outcome = \core_course\management\helper::action_category_show_by_id($categoryid);
$outcome->categoryvisibility = \core_course\management\helper::get_category_children_visibility($categoryid);
$outcome->coursevisibility = \core_course\management\helper::get_category_courses_visibility($categoryid);
if ($selectedcategoryid !== null) {
$outcome->coursevisibility = array_merge(
$outcome->coursevisibility,
\core_course\management\helper::get_category_courses_visibility($selectedcategoryid)
);
}
break;
case 'expandcategory':
$categoryid = required_param('categoryid', PARAM_INT);
$coursecat = core_course_category::get($categoryid);
\core_course\management\helper::record_expanded_category($coursecat);
$outcome->outcome = true;
break;
case 'collapsecategory':
$categoryid = required_param('categoryid', PARAM_INT);
$coursecat = core_course_category::get($categoryid);
\core_course\management\helper::record_expanded_category($coursecat, false);
$outcome->outcome = true;
break;
case 'getsubcategorieshtml' :
$categoryid = required_param('categoryid', PARAM_INT);
/* @var core_course_management_renderer $renderer */
$renderer = $PAGE->get_renderer('core_course', 'management');
$outcome->html = html_writer::start_tag('ul',
array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoriesof'.$categoryid));
$coursecat = core_course_category::get($categoryid);
foreach ($coursecat->get_children() as $subcat) {
$outcome->html .= $renderer->category_listitem($subcat, array(), $subcat->get_children_count());
}
$outcome->html .= html_writer::end_tag('ul');
$outcome->outcome = true;
break;
}
echo json_encode($outcome);
echo $OUTPUT->footer();
// Thats all folks.
// Don't ever even consider putting anything after this. It just wouldn't make sense.
// But you already knew that, you smart developer you.
exit;
+11
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
define("core_course/copy_modal",["exports","core/str","core/modal","core/ajax","core/fragment","core/notification","core/config"],(function(_exports,_str,_modal,ajax,Fragment,_notification,Config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* This module provides the course copy modal from the course and
* category management screen.
*
* @module core_course/copy_modal
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.9
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),ajax=_interopRequireWildcard(ajax),Fragment=_interopRequireWildcard(Fragment),_notification=_interopRequireDefault(_notification),Config=_interopRequireWildcard(Config);class CopyModal{static init(context){return new CopyModal(context)}constructor(context){this.contextid=context,this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(e=>{const copyAction=e.target.closest(".action-copy");if(!copyAction)return;e.preventDefault();const url=new URL(copyAction.href),params=new URLSearchParams(url.search);this.fetchCourseData(params.get("id")).then((_ref=>{let[course]=_ref;return this.createModal(course)})).catch((error=>_notification.default.exception(error)))}))}fetchCourseData(courseid){return ajax.call([{methodname:"core_course_get_courses",args:{options:{ids:[courseid]}}}])[0]}submitBackupRequest(jsonformdata){return ajax.call([{methodname:"core_backup_submit_copy_form",args:{jsonformdata:jsonformdata}}])[0]}createModal(course){let formdata=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const params={jsonformdata:JSON.stringify(formdata),courseid:course.id};return _modal.default.create({title:(0,_str.get_string)("copycoursetitle","backup",course.shortname),body:Fragment.loadFragment("course","new_base_form",this.contextid,params),large:!0,show:!0,removeOnClose:!0}).then((modal=>(modal.getRoot().on("click","#id_submitreturn",(e=>{this.processModalForm(course,modal,e)})),modal.getRoot().on("click","#id_cancel",(e=>{e.preventDefault(),modal.destroy()})),modal.getRoot().on("click","#id_submitdisplay",(e=>{e.formredirect=!0,this.processModalForm(course,modal,e)})),modal)))}processModalForm(course,modal,e){e.preventDefault();const copyform=modal.getRoot().find("form").serialize(),formjson=JSON.stringify(copyform),invalid=modal.getRoot()[0].querySelectorAll('[aria-invalid="true"], .error');invalid.length?invalid[0].focus():(modal.destroy(),this.submitBackupRequest(formjson).then((()=>{if(1==e.formredirect){const redirect="".concat(Config.wwwroot,"/backup/copyprogress.php?id=").concat(course.id);window.location.assign(redirect)}})).catch((()=>{this.createModal(course,copyform)})))}}return _exports.default=CopyModal,_exports.default}));
//# sourceMappingURL=copy_modal.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/downloadcontent",["exports","core/config","core/custom_interaction_events","core/modal_save_cancel","jquery","core/pending","core/key_codes"],(function(_exports,_config,_custom_interaction_events,_modal_save_cancel,_jquery,_pending,_key_codes){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Functions related to downloading course content.
*
* @module core_course/downloadcontent
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_config=_interopRequireDefault(_config),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);_exports.init=()=>{const pendingPromise=new _pending.default;(0,_jquery.default)("[data-downloadcourse]").on("click keydown",(e=>{"click"!==e.type&&e.which!==_key_codes.enter&&e.which!==_key_codes.space||(e.preventDefault(),displayDownloadConfirmation(e.currentTarget))})),pendingPromise.resolve()};const displayDownloadConfirmation=downloadModalTrigger=>_modal_save_cancel.default.create({title:downloadModalTrigger.dataset.downloadTitle,body:"<p>".concat(downloadModalTrigger.dataset.downloadBody,"</p>"),buttons:{save:downloadModalTrigger.dataset.downloadButtonText},templateContext:{classes:"downloadcoursecontentmodal"}}).then((modal=>{modal.show();const saveButton=document.querySelector('.modal .downloadcoursecontentmodal [data-action="save"]'),cancelButton=document.querySelector('.modal .downloadcoursecontentmodal [data-action="cancel"]'),modalContainer=document.querySelector('.modal[data-region="modal-container"]');return(0,_jquery.default)(saveButton).on(_custom_interaction_events.default.events.activate,(e=>downloadContent(e,downloadModalTrigger,modal))),(0,_jquery.default)(cancelButton).on(_custom_interaction_events.default.events.activate,(()=>{modal.destroy()})),modalContainer.querySelector(".downloadcoursecontentmodal")&&(0,_jquery.default)(modalContainer).on(_custom_interaction_events.default.events.activate,(()=>{modal.destroy()})),modal})),downloadContent=(e,downloadModalTrigger,modal)=>{e.preventDefault();const downloadForm=document.createElement("form");downloadForm.action=downloadModalTrigger.dataset.downloadLink,downloadForm.method="POST",downloadForm.target="_blank";const downloadSesskey=document.createElement("input");downloadSesskey.name="sesskey",downloadSesskey.value=_config.default.sesskey,downloadForm.appendChild(downloadSesskey),downloadForm.style.display="none",document.body.appendChild(downloadForm),downloadForm.submit(),document.body.removeChild(downloadForm),modal.destroy()}}));
//# sourceMappingURL=downloadcontent.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("core_course/events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled",stateChanged:"core_course:stateChanged",sectionRefreshed:"core_course:sectionRefreshed"},_exports.default}));
//# sourceMappingURL=events.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @copyright 2018 Simey Lameze <simey@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n stateChanged: 'core_course:stateChanged',\n sectionRefreshed: 'core_course:sectionRefreshed',\n};\n"],"names":["favourited","unfavorited","manualCompletionToggled","stateChanged","sectionRefreshed"],"mappings":"oKAsBe,CACXA,WAAY,yBACZC,YAAa,0BACbC,wBAAyB,sCACzBC,aAAc,2BACdC,iBAAkB"}
+12
View File
@@ -0,0 +1,12 @@
define("core_course/formatchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Course format selection handler.
*
* @module core_course/formatchooser
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
const Selectors_fields={selector:'[data-formatchooser-field="selector"]',updateButton:'[data-formatchooser-field="updateButton"]'};_exports.init=()=>{document.querySelector(Selectors_fields.selector).addEventListener("change",(e=>{const form=e.target.closest("form"),updateButton=form.querySelector(Selectors_fields.updateButton),fieldset=updateButton.closest("fieldset"),url=new URL(form.action);url.hash=fieldset.id,form.action=url.toString(),updateButton.click()}))}}));
//# sourceMappingURL=formatchooser.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"formatchooser.min.js","sources":["../src/formatchooser.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course format selection handler.\n *\n * @module core_course/formatchooser\n * @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-formatchooser-field=\"selector\"]',\n updateButton: '[data-formatchooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the format chooser.\n */\nexport const init = () => {\n document.querySelector(Selectors.fields.selector).addEventListener('change', e => {\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n const fieldset = updateButton.closest('fieldset');\n\n const url = new URL(form.action);\n url.hash = fieldset.id;\n\n form.action = url.toString();\n updateButton.click();\n });\n};\n"],"names":["Selectors","selector","updateButton","document","querySelector","addEventListener","e","form","target","closest","fieldset","url","URL","action","hash","id","toString","click"],"mappings":";;;;;;;;;MAuBMA,iBACM,CACJC,SAAU,wCACVC,aAAc,2DAOF,KAChBC,SAASC,cAAcJ,iBAAiBC,UAAUI,iBAAiB,UAAUC,UACnEC,KAAOD,EAAEE,OAAOC,QAAQ,QACxBP,aAAeK,KAAKH,cAAcJ,iBAAiBE,cACnDQ,SAAWR,aAAaO,QAAQ,YAEhCE,IAAM,IAAIC,IAAIL,KAAKM,QACzBF,IAAIG,KAAOJ,SAASK,GAEpBR,KAAKM,OAASF,IAAIK,WAClBd,aAAae"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
define("core_course/local/activitychooser/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle user AJAX actions.
*
* @module core_course/local/activitychooser/repository
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unfavouriteModule=_exports.fetchFooterData=_exports.favouriteModule=_exports.activityModules=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.activityModules=courseid=>{const request={methodname:"core_course_get_course_content_items",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.favouriteModule=(modName,modID)=>{const request={methodname:"core_course_add_content_item_to_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.unfavouriteModule=(modName,modID)=>{const request={methodname:"core_course_remove_content_item_from_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.fetchFooterData=(courseid,sectionid)=>{const request={methodname:"core_course_get_activity_chooser_footer",args:{courseid:courseid,sectionid:sectionid}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../../src/local/activitychooser/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle user AJAX actions.\n *\n * @module core_course/local/activitychooser/repository\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method activityModules\n * @param {Number} courseid What course to fetch the modules for\n * @return {object} jQuery promise\n */\nexport const activityModules = (courseid) => {\n const request = {\n methodname: 'core_course_get_course_content_items',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is a users' favourite.\n *\n * @method favouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const favouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_add_content_item_to_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is no longer a users' favourite.\n *\n * @method unfavouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const unfavouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_remove_content_item_from_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchFooterData\n * @param {Number} courseid What course to fetch the data for\n * @param {Number} sectionid What section to fetch the data for\n * @return {object} jQuery promise\n */\nexport const fetchFooterData = (courseid, sectionid) => {\n const request = {\n methodname: 'core_course_get_activity_chooser_footer',\n args: {\n courseid: courseid,\n sectionid: sectionid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call","modName","modID","componentname","contentitemid","sectionid"],"mappings":";;;;;;;uPA+BgCA,iBACtBC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU,6BAYD,CAACK,QAASC,eAC/BN,QAAU,CACZC,WAAY,kDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,+BAYC,CAACK,QAASC,eACjCN,QAAU,CACZC,WAAY,uDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,6BAWD,CAACD,SAAUU,mBAChCT,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFH,SAAUA,SACVU,UAAWA,mBAGZN,cAAKC,KAAK,CAACJ,UAAU"}
+11
View File
@@ -0,0 +1,11 @@
define("core_course/local/activitychooser/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module core_course/local/activitychooser/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{chooser:getDataSelector("region","chooser-container"),getSectionChooserOptions:containerid=>"".concat(containerid," ").concat(getDataSelector("region","chooser-options-container")),chooserOption:{container:getDataSelector("region","chooser-option-container"),actions:getDataSelector("region","chooser-option-actions-container"),info:getDataSelector("region","chooser-option-info-container")},chooserSummary:{container:getDataSelector("region","chooser-option-summary-container"),content:getDataSelector("region","chooser-option-summary-content-container"),header:getDataSelector("region","summary-header"),actions:getDataSelector("region","chooser-option-summary-actions-container")},carousel:getDataSelector("region","carousel"),help:getDataSelector("region","help"),modules:getDataSelector("region","modules"),favouriteTabNav:getDataSelector("region","favourite-tab-nav"),defaultTabNav:getDataSelector("region","default-tab-nav"),activityTabNav:getDataSelector("region","activity-tab-nav"),favouriteTab:getDataSelector("region","favourites"),recommendedTab:getDataSelector("region","recommended"),defaultTab:getDataSelector("region","default"),activityTab:getDataSelector("region","activity"),resourceTab:getDataSelector("region","resources"),getModuleSelector:modname=>'[role="menuitem"][data-modname="'.concat(modname,'"]'),searchResults:getDataSelector("region","search-results-container"),searchResultItems:getDataSelector("region","search-result-items-container")},actions:{optionActions:{showSummary:getDataSelector("action","show-option-summary"),manageFavourite:getDataSelector("action","manage-module-favourite")},addChooser:getDataSelector("action","add-chooser-option"),closeOption:getDataSelector("action","close-chooser-option-summary"),hide:getDataSelector("action","hide"),search:getDataSelector("action","search"),clearSearch:getDataSelector("action","clearsearch")},render:{favourites:getDataSelector("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:'a[data-toggle="tab"]',activetab:'a[data-toggle="tab"][aria-selected="true"]',visibletabs:'a[data-toggle="tab"]:not(.d-none)'}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=selectors.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("core_course/manual_completion_toggle",["exports","core/templates","core/notification","core_course/repository","core_course/events","core/pending"],(function(_exports,_templates,_notification,_repository,CourseEvents,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Provides the functionality for toggling the manual completion state of a course module through
* the manual completion button.
*
* @module core_course/manual_completion_toggle
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const pendingPromise=new _pending.default("core_course:toggleManualCompletionState"),originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE;_templates.default.renderForPromise("core/loading",{}).then((loadingHtml=>{_templates.default.replaceNodeContents(toggleButton,loadingHtml,"")})).catch((()=>{}));try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}pendingPromise.resolve()}}));
//# sourceMappingURL=manual_completion_toggle.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/recommendations",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A javascript module to handle toggling activity chooser recommendations.
*
* @module core_course/recommendations
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);const toggleRecommendation=e=>{let data={methodname:"core_course_toggle_activity_recommendation",args:{area:e.currentTarget.dataset.area,id:e.currentTarget.dataset.id}};_ajax.default.call([data])[0].fail(_notification.default.exception)};_exports.init=()=>{document.querySelectorAll("[data-area]").forEach((checkbox=>{checkbox.addEventListener("change",toggleRecommendation)}))}}));
//# sourceMappingURL=recommendations.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"recommendations.min.js","sources":["../src/recommendations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle toggling activity chooser recommendations.\n *\n * @module core_course/recommendations\n * @copyright 2020 Adrian Greeve <adrian@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Do an ajax call to toggle the recommendation\n *\n * @param {object} e The event\n * @return {void}\n */\nconst toggleRecommendation = (e) => {\n let data = {\n methodname: 'core_course_toggle_activity_recommendation',\n args: {\n area: e.currentTarget.dataset.area,\n id: e.currentTarget.dataset.id\n }\n };\n Ajax.call([data])[0].fail(Notification.exception);\n};\n\n/**\n * Initialisation function\n *\n * @return {void}\n */\nexport const init = () => {\n const checkboxelements = document.querySelectorAll(\"[data-area]\");\n checkboxelements.forEach((checkbox) => {\n checkbox.addEventListener('change', toggleRecommendation);\n });\n};\n"],"names":["toggleRecommendation","e","data","methodname","args","area","currentTarget","dataset","id","call","fail","Notification","exception","document","querySelectorAll","forEach","checkbox","addEventListener"],"mappings":";;;;;;;gLAgCMA,qBAAwBC,QACtBC,KAAO,CACPC,WAAY,6CACZC,KAAM,CACFC,KAAMJ,EAAEK,cAAcC,QAAQF,KAC9BG,GAAIP,EAAEK,cAAcC,QAAQC,mBAG/BC,KAAK,CAACP,OAAO,GAAGQ,KAAKC,sBAAaC,0BAQvB,KACSC,SAASC,iBAAiB,eAClCC,SAASC,WACtBA,SAASC,iBAAiB,SAAUjB"}
+10
View File
@@ -0,0 +1,10 @@
define("core_course/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle course ajax actions.
*
* @module core_course/repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:function(cmid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},getGradableUsersFromCourseID:function(courseid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"core_grades_get_gradable_users",args:{courseid:courseid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=repository.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/view",["exports","core_course/events"],(function(_exports,CourseEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* JS module for the course homepage.
*
* @module core_course/view
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(CourseEvents);let registered=!1;_exports.init=()=>{registered||(document.addEventListener(CourseEvents.manualCompletionToggled,(e=>{parseInt(e.detail.withAvailability)&&window.location.reload()})),registered=!0)}}));
//# sourceMappingURL=view.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"view.min.js","sources":["../src/view.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * JS module for the course homepage.\n *\n * @module core_course/view\n * @copyright 2021 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as CourseEvents from 'core_course/events';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n/**\n * Function to intialise and register event listeners for this module.\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n // Listen for toggled manual completion states of activities.\n document.addEventListener(CourseEvents.manualCompletionToggled, (e) => {\n const withAvailability = parseInt(e.detail.withAvailability);\n if (withAvailability) {\n // Reload the page when the toggled manual completion button has availability conditions linked to it.\n window.location.reload();\n }\n });\n registered = true;\n};\n"],"names":["registered","document","addEventListener","CourseEvents","manualCompletionToggled","e","parseInt","detail","withAvailability","window","location","reload"],"mappings":";;;;;;;wBA8BIA,YAAa,gBAKG,KACZA,aAIJC,SAASC,iBAAiBC,aAAaC,yBAA0BC,IACpCC,SAASD,EAAEE,OAAOC,mBAGvCC,OAAOC,SAASC,YAGxBX,YAAa"}
File diff suppressed because it is too large Load Diff
+421
View File
@@ -0,0 +1,421 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A type of dialogue used as for choosing modules in a course.
*
* @module core_course/activitychooser
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';
import * as Repository from 'core_course/local/activitychooser/repository';
import selectors from 'core_course/local/activitychooser/selectors';
import CustomEvents from 'core/custom_interaction_events';
import * as Templates from 'core/templates';
import {getString} from 'core/str';
import Modal from 'core/modal';
import Pending from 'core/pending';
// Set up some JS module wide constants that can be added to in the future.
// Tab config options.
const ALLACTIVITIESRESOURCES = 0;
const ACTIVITIESRESOURCES = 2;
const ALLACTIVITIESRESOURCESREC = 3;
const ONLYALLREC = 4;
const ACTIVITIESRESOURCESREC = 5;
// Module types.
const ACTIVITY = 0;
const RESOURCE = 1;
let initialized = false;
/**
* Set up the activity chooser.
*
* @method init
* @param {Number} courseId Course ID to use later on in fetchModules()
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
*/
export const init = (courseId, chooserConfig) => {
const pendingPromise = new Pending();
registerListenerEvents(courseId, chooserConfig);
pendingPromise.resolve();
};
/**
* Once a selection has been made make the modal & module information and pass it along
*
* @method registerListenerEvents
* @param {Number} courseId
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
*/
const registerListenerEvents = (courseId, chooserConfig) => {
// Ensure we only add our listeners once.
if (initialized) {
return;
}
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
const fetchModuleData = (() => {
let innerPromise = null;
return () => {
if (!innerPromise) {
innerPromise = new Promise((resolve) => {
resolve(Repository.activityModules(courseId));
});
}
return innerPromise;
};
})();
const fetchFooterData = (() => {
let footerInnerPromise = null;
return (sectionId) => {
if (!footerInnerPromise) {
footerInnerPromise = new Promise((resolve) => {
resolve(Repository.fetchFooterData(courseId, sectionId));
});
}
return footerInnerPromise;
};
})();
CustomEvents.define(document, events);
// Display module chooser event listeners.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
if (e.target.closest(selectors.elements.sectionmodchooser)) {
let caller;
// We need to know who called this.
// Standard courses use the ID in the main section info.
const sectionDiv = e.target.closest(selectors.elements.section);
// Front page courses need some special handling.
const button = e.target.closest(selectors.elements.sectionmodchooser);
// If we don't have a section ID use the fallback ID.
// We always want the sectionDiv caller first as it keeps track of section ID's after DnD changes.
// The button attribute is always just a fallback for us as the section div is not always available.
// A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.
if (sectionDiv !== null && sectionDiv.hasAttribute('data-sectionid')) {
// We check for attributes just in case of outdated contrib course formats.
caller = sectionDiv;
} else {
caller = button;
}
// We want to show the modal instantly but loading whilst waiting for our data.
let bodyPromiseResolver;
const bodyPromise = new Promise(resolve => {
bodyPromiseResolver = resolve;
});
const footerData = await fetchFooterData(caller.dataset.sectionid);
const sectionModal = buildModal(bodyPromise, footerData);
// Now we have a modal we should start fetching data.
// If an error occurs while fetching the data, display the error within the modal.
const data = await fetchModuleData().catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));
});
// Early return if there is no module data.
if (!data) {
return;
}
// Apply the section id to all the module instance links.
const builtModuleData = sectionIdMapper(
data,
caller.dataset.sectionid,
caller.dataset.sectionreturnid,
caller.dataset.beforemod
);
ChooserDialogue.displayChooser(
sectionModal,
builtModuleData,
partiallyAppliedFavouriteManager(data, caller.dataset.sectionid),
footerData,
);
bodyPromiseResolver(await Templates.render(
'core_course/activitychooser',
templateDataBuilder(builtModuleData, chooserConfig)
));
}
});
});
initialized = true;
};
/**
* Given the web service data and an ID we want to make a deep copy
* of the WS data then add on the section ID to the addoption URL
*
* @method sectionIdMapper
* @param {Object} webServiceData Our original data from the Web service call
* @param {Number} id The ID of the section we need to append to the links
* @param {Number|null} sectionreturnid The ID of the section return we need to append to the links
* @param {Number|null} beforemod The ID of the cm we need to append to the links
* @return {Array} [modules] with URL's built
*/
const sectionIdMapper = (webServiceData, id, sectionreturnid, beforemod) => {
// We need to take a fresh deep copy of the original data as an object is a reference type.
const newData = JSON.parse(JSON.stringify(webServiceData));
newData.content_items.forEach((module) => {
module.link += '&section=' + id + '&beforemod=' + (beforemod ?? 0);
if (sectionreturnid) {
module.link += '&sr=' + sectionreturnid;
}
});
return newData.content_items;
};
/**
* Given an array of modules we want to figure out where & how to place them into our template object
*
* @method templateDataBuilder
* @param {Array} data our modules to manipulate into a Templatable object
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
* @return {Object} Our built object ready to render out
*/
const templateDataBuilder = (data, chooserConfig) => {
// Setup of various bits and pieces we need to mutate before throwing it to the wolves.
let activities = [];
let resources = [];
let showAll = true;
let showActivities = false;
let showResources = false;
// Tab mode can be the following [All, Resources & Activities, All & Activities & Resources].
const tabMode = parseInt(chooserConfig.tabmode);
// Filter the incoming data to find favourite & recommended modules.
const favourites = data.filter(mod => mod.favourite === true);
const recommended = data.filter(mod => mod.recommended === true);
// Whether the activities and resources tabs should be displayed or not.
const showActivitiesAndResources = (tabMode) => {
const acceptableModes = [
ALLACTIVITIESRESOURCES,
ALLACTIVITIESRESOURCESREC,
ACTIVITIESRESOURCES,
ACTIVITIESRESOURCESREC,
];
return acceptableModes.indexOf(tabMode) !== -1;
};
// These modes need Activity & Resource tabs.
if (showActivitiesAndResources(tabMode)) {
// Filter the incoming data to find activities then resources.
activities = data.filter(mod => mod.archetype === ACTIVITY);
resources = data.filter(mod => mod.archetype === RESOURCE);
showActivities = true;
showResources = true;
// We want all of the previous information but no 'All' tab.
if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) {
showAll = false;
}
}
const recommendedBeforeTabs = [
ALLACTIVITIESRESOURCESREC,
ONLYALLREC,
ACTIVITIESRESOURCESREC,
];
// Whether the recommended tab should be displayed before the All/Activities/Resources tabs.
const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1;
// Given the results of the above filters lets figure out what tab to set active.
// We have some favourites.
const favouritesFirst = !!favourites.length;
const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length;
// We are in tabMode 2 without any favourites.
const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false;
// We have nothing fallback to show all modules.
const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false;
return {
'default': data,
showAll: showAll,
activities: activities,
showActivities: showActivities,
activitiesFirst: activitiesFirst,
resources: resources,
showResources: showResources,
favourites: favourites,
recommended: recommended,
recommendedFirst: recommendedFirst,
recommendedBeginning: recommendedBeginning,
favouritesFirst: favouritesFirst,
fallback: fallback,
};
};
/**
* Given an object we want to build a modal ready to show
*
* @method buildModal
* @param {Promise} body
* @param {String|Boolean} footer Either a footer to add or nothing
* @return {Object} The modal ready to display immediately and render body in later.
*/
const buildModal = (body, footer) => Modal.create({
body,
title: getString('addresourceoractivity'),
footer: footer.customfootertemplate,
large: true,
scrollable: false,
templateContext: {
classes: 'modchooser'
},
show: true,
});
/**
* A small helper function to handle the case where there are no more favourites
* and we need to mess a bit with the available tabs in the chooser
*
* @method nullFavouriteDomManager
* @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav
* @param {HTMLElement} modalBody Our current modals' body
*/
const nullFavouriteDomManager = (favouriteTabNav, modalBody) => {
favouriteTabNav.tabIndex = -1;
favouriteTabNav.classList.add('d-none');
// Need to set active to an available tab.
if (favouriteTabNav.classList.contains('active')) {
favouriteTabNav.classList.remove('active');
favouriteTabNav.setAttribute('aria-selected', 'false');
const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);
favouriteTab.classList.remove('active');
const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);
const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav);
if (defaultTabNav.classList.contains('d-none') === false) {
defaultTabNav.classList.add('active');
defaultTabNav.setAttribute('aria-selected', 'true');
defaultTabNav.tabIndex = 0;
defaultTabNav.focus();
const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);
defaultTab.classList.add('active');
} else {
activitiesTabNav.classList.add('active');
activitiesTabNav.setAttribute('aria-selected', 'true');
activitiesTabNav.tabIndex = 0;
activitiesTabNav.focus();
const activitiesTab = modalBody.querySelector(selectors.regions.activityTab);
activitiesTab.classList.add('active');
}
}
};
/**
* Export a curried function where the builtModules has been applied.
* We have our array of modules so we can rerender the favourites area and have all of the items sorted.
*
* @method partiallyAppliedFavouriteManager
* @param {Array} moduleData This is our raw WS data that we need to manipulate
* @param {Number} sectionId We need this to add the sectionID to the URL's in the faves area after rerender
* @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array
*/
const partiallyAppliedFavouriteManager = (moduleData, sectionId) => {
/**
* Curried function that is being returned.
*
* @param {String} internal Internal name of the module to manage
* @param {Boolean} favourite Is the caller adding a favourite or removing one?
* @param {HTMLElement} modalBody What we need to update whilst we are here
*/
return async(internal, favourite, modalBody) => {
const favouriteArea = modalBody.querySelector(selectors.render.favourites);
// eslint-disable-next-line max-len
const favouriteButtons = modalBody.querySelectorAll(`[data-internal="${internal}"] ${selectors.actions.optionActions.manageFavourite}`);
const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);
const result = moduleData.content_items.find(({name}) => name === internal);
const newFaves = {};
if (result) {
if (favourite) {
result.favourite = true;
// eslint-disable-next-line camelcase
newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);
const builtFaves = sectionIdMapper(newFaves, sectionId);
const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites',
{favourites: builtFaves});
await Templates.replaceNodeContents(favouriteArea, html, js);
Array.from(favouriteButtons).forEach((element) => {
element.classList.remove('text-muted');
element.classList.add('text-primary');
element.dataset.favourited = 'true';
element.setAttribute('aria-pressed', true);
element.firstElementChild.classList.remove('fa-star-o');
element.firstElementChild.classList.add('fa-star');
});
favouriteTabNav.classList.remove('d-none');
} else {
result.favourite = false;
const nodeToRemove = favouriteArea.querySelector(`[data-internal="${internal}"]`);
nodeToRemove.parentNode.removeChild(nodeToRemove);
Array.from(favouriteButtons).forEach((element) => {
element.classList.add('text-muted');
element.classList.remove('text-primary');
element.dataset.favourited = 'false';
element.setAttribute('aria-pressed', false);
element.firstElementChild.classList.remove('fa-star');
element.firstElementChild.classList.add('fa-star-o');
});
const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);
if (newFaves.length === 0) {
nullFavouriteDomManager(favouriteTabNav, modalBody);
}
}
}
};
};
+150
View File
@@ -0,0 +1,150 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This module provides the course copy modal from the course and
* category management screen.
*
* @module core_course/copy_modal
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.9
*/
import {get_string as getString} from 'core/str';
import Modal from 'core/modal';
import * as ajax from 'core/ajax';
import * as Fragment from 'core/fragment';
import Notification from 'core/notification';
import * as Config from 'core/config';
export default class CopyModal {
static init(context) {
return new CopyModal(context);
}
constructor(context) {
this.contextid = context;
this.registerEventListeners();
}
registerEventListeners() {
document.addEventListener('click', (e) => {
const copyAction = e.target.closest('.action-copy');
if (!copyAction) {
return;
}
e.preventDefault(); // Stop. Hammer time.
const url = new URL(copyAction.href);
const params = new URLSearchParams(url.search);
this.fetchCourseData(params.get('id'))
.then(([course]) => this.createModal(course))
.catch((error) => Notification.exception(error));
});
}
fetchCourseData(courseid) {
return ajax.call([{
methodname: 'core_course_get_courses',
args: {
options: {
ids: [courseid],
},
},
}])[0];
}
submitBackupRequest(jsonformdata) {
return ajax.call([{
methodname: 'core_backup_submit_copy_form',
args: {
jsonformdata,
},
}])[0];
}
createModal(
course,
formdata = {},
) {
const params = {
jsonformdata: JSON.stringify(formdata),
courseid: course.id,
};
// Create the Modal.
return Modal.create({
title: getString('copycoursetitle', 'backup', course.shortname),
body: Fragment.loadFragment('course', 'new_base_form', this.contextid, params),
large: true,
show: true,
removeOnClose: true,
})
.then((modal) => {
// Explicitly handle form click events.
modal.getRoot().on('click', '#id_submitreturn', (e) => {
this.processModalForm(course, modal, e);
});
modal.getRoot().on('click', '#id_cancel', (e) => {
e.preventDefault();
modal.destroy();
});
modal.getRoot().on('click', '#id_submitdisplay', (e) => {
e.formredirect = true;
this.processModalForm(course, modal, e);
});
return modal;
});
}
processModalForm(course, modal, e) {
e.preventDefault(); // Stop modal from closing.
// Form data.
const copyform = modal.getRoot().find('form').serialize();
const formjson = JSON.stringify(copyform);
// Handle invalid form fields for better UX.
const invalid = modal.getRoot()[0].querySelectorAll('[aria-invalid="true"], .error');
if (invalid.length) {
invalid[0].focus();
return;
}
modal.destroy();
// Submit form via ajax.
this.submitBackupRequest(formjson)
.then(() => {
if (e.formredirect == true) {
// We are redirecting to copy progress display.
const redirect = `${Config.wwwroot}/backup/copyprogress.php?id=${course.id}`;
window.location.assign(redirect);
}
return;
})
.catch(() => {
// Form submission failed server side, redisplay with errors.
this.createModal(course, copyform);
});
}
}
+125
View File
@@ -0,0 +1,125 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Functions related to downloading course content.
*
* @module core_course/downloadcontent
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Config from 'core/config';
import CustomEvents from 'core/custom_interaction_events';
import SaveCancelModal from 'core/modal_save_cancel';
import jQuery from 'jquery';
import Pending from 'core/pending';
import {enter, space} from 'core/key_codes';
/**
* Set up listener to trigger the download course content modal.
*
* @return {void}
*/
export const init = () => {
const pendingPromise = new Pending();
// Add event listeners for click and enter/space keys.
jQuery('[data-downloadcourse]').on('click keydown', (e) => {
if (e.type === 'click' || e.which === enter || e.which === space) {
e.preventDefault();
displayDownloadConfirmation(e.currentTarget);
}
});
pendingPromise.resolve();
};
/**
* Display the download course content modal.
*
* @method displayDownloadConfirmation
* @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
* @return {void}
*/
const displayDownloadConfirmation = (downloadModalTrigger) => {
return SaveCancelModal.create({
title: downloadModalTrigger.dataset.downloadTitle,
body: `<p>${downloadModalTrigger.dataset.downloadBody}</p>`,
buttons: {
save: downloadModalTrigger.dataset.downloadButtonText
},
templateContext: {
classes: 'downloadcoursecontentmodal'
}
})
.then((modal) => {
// Display the modal.
modal.show();
const saveButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="save"]');
const cancelButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="cancel"]');
const modalContainer = document.querySelector('.modal[data-region="modal-container"]');
// Create listener to trigger the download when the "Download" button is pressed.
jQuery(saveButton).on(CustomEvents.events.activate, (e) => downloadContent(e, downloadModalTrigger, modal));
// Create listener to destroy the modal when closing modal by cancelling.
jQuery(cancelButton).on(CustomEvents.events.activate, () => {
modal.destroy();
});
// Create listener to destroy the modal when closing modal by clicking outside of it.
if (modalContainer.querySelector('.downloadcoursecontentmodal')) {
jQuery(modalContainer).on(CustomEvents.events.activate, () => {
modal.destroy();
});
}
return modal;
});
};
/**
* Trigger downloading of course content.
*
* @method downloadContent
* @param {Event} e The event triggering the download.
* @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
* @param {Object} modal The modal object.
* @return {void}
*/
const downloadContent = (e, downloadModalTrigger, modal) => {
e.preventDefault();
// Create a form to submit the file download request, so we can avoid sending sesskey over GET.
const downloadForm = document.createElement('form');
downloadForm.action = downloadModalTrigger.dataset.downloadLink;
downloadForm.method = 'POST';
// Open download in a new tab, so current course view is not disrupted.
downloadForm.target = '_blank';
const downloadSesskey = document.createElement('input');
downloadSesskey.name = 'sesskey';
downloadSesskey.value = Config.sesskey;
downloadForm.appendChild(downloadSesskey);
downloadForm.style.display = 'none';
document.body.appendChild(downloadForm);
downloadForm.submit();
document.body.removeChild(downloadForm);
// Destroy the modal to prevent duplicates if reopened later.
modal.destroy();
};
+29
View File
@@ -0,0 +1,29 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contain the events the course component can trigger.
*
* @module core_course/events
* @copyright 2018 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
favourited: 'core_course:favourited',
unfavorited: 'core_course:unfavorited',
manualCompletionToggled: 'core_course:manualcompletiontoggled',
stateChanged: 'core_course:stateChanged',
sectionRefreshed: 'core_course:sectionRefreshed',
};
+46
View File
@@ -0,0 +1,46 @@
// This file is part of Moodle - http://moodle.org/ //
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course format selection handler.
*
* @module core_course/formatchooser
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
const Selectors = {
fields: {
selector: '[data-formatchooser-field="selector"]',
updateButton: '[data-formatchooser-field="updateButton"]',
},
};
/**
* Initialise the format chooser.
*/
export const init = () => {
document.querySelector(Selectors.fields.selector).addEventListener('change', e => {
const form = e.target.closest('form');
const updateButton = form.querySelector(Selectors.fields.updateButton);
const fieldset = updateButton.closest('fieldset');
const url = new URL(form.action);
url.hash = fieldset.id;
form.action = url.toString();
updateButton.click();
});
};
@@ -0,0 +1,524 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A type of dialogue used as for choosing options.
*
* @module core_course/local/activitychooser/dialogue
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import * as ModalEvents from 'core/modal_events';
import selectors from 'core_course/local/activitychooser/selectors';
import * as Templates from 'core/templates';
import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
import {addIconToContainer} from 'core/loadingicon';
import * as Repository from 'core_course/local/activitychooser/repository';
import Notification from 'core/notification';
import {debounce} from 'core/utils';
const getPlugin = pluginName => import(pluginName);
/**
* Given an event from the main module 'page' navigate to it's help section via a carousel.
*
* @method showModuleHelp
* @param {jQuery} carousel Our initialized carousel to manipulate
* @param {Object} moduleData Data of the module to carousel to
* @param {jQuery} modal We need to figure out if the current modal has a footer.
*/
const showModuleHelp = (carousel, moduleData, modal = null) => {
// If we have a real footer then we need to change temporarily.
if (modal !== null && moduleData.showFooter === true) {
modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));
}
const help = carousel.find(selectors.regions.help)[0];
help.innerHTML = '';
help.classList.add('m-auto');
// Add a spinner.
const spinnerPromise = addIconToContainer(help);
// Used later...
let transitionPromiseResolver = null;
const transitionPromise = new Promise(resolve => {
transitionPromiseResolver = resolve;
});
// Build up the html & js ready to place into the help section.
const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);
// Wait for the content to be ready, and for the transition to be complet.
Promise.all([contentPromise, spinnerPromise, transitionPromise])
.then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
.then(() => {
help.querySelector(selectors.regions.chooserSummary.header).focus();
return help;
})
.catch(Notification.exception);
// Move to the next slide, and resolve the transition promise when it's done.
carousel.one('slid.bs.carousel', () => {
transitionPromiseResolver();
});
// Trigger the transition between 'pages'.
carousel.carousel('next');
};
/**
* Given a user wants to change the favourite state of a module we either add or remove the status.
* We also propergate this change across our map of modals.
*
* @method manageFavouriteState
* @param {HTMLElement} modalBody The DOM node of the modal to manipulate
* @param {HTMLElement} caller
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
*/
const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
const isFavourite = caller.dataset.favourited;
const id = caller.dataset.id;
const name = caller.dataset.name;
const internal = caller.dataset.internal;
// Switch on fave or not.
if (isFavourite === 'true') {
await Repository.unfavouriteModule(name, id);
partialFavourite(internal, false, modalBody);
} else {
await Repository.favouriteModule(name, id);
partialFavourite(internal, true, modalBody);
}
};
/**
* Register chooser related event listeners.
*
* @method registerListenerEvents
* @param {Promise} modal Our modal that we are working with
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
* @param {Object} footerData Our base footer object.
*/
const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {
const bodyClickListener = async(e) => {
if (e.target.closest(selectors.actions.optionActions.showSummary)) {
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
const module = e.target.closest(selectors.regions.chooserOption.container);
const moduleName = module.dataset.modname;
const moduleData = mappedModules.get(moduleName);
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
moduleData.showFooter = modal.hasFooterContent();
showModuleHelp(carousel, moduleData, modal);
}
if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href");
const sectionChooserOptions = modal.getBody()[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = sectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);
}
// From the help screen go back to the module overview.
if (e.target.matches(selectors.actions.closeOption)) {
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
// Trigger the transition between 'pages'.
carousel.carousel('prev');
carousel.on('slid.bs.carousel', () => {
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
caller.focus();
});
}
// The "clear search" button is triggered.
if (e.target.closest(selectors.actions.clearSearch)) {
// Clear the entered search query in the search bar and hide the search results container.
const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);
searchInput.value = "";
searchInput.focus();
toggleSearchResultsView(modal, mappedModules, searchInput.value);
}
};
// We essentially have two types of footer.
// A fake one that is handled within the template for chooser_help and then all of the stuff for
// modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we
// need to manage. The below code handles a real footer going to a mnet carousel item.
const footerClickListener = async(e) => {
if (footerData.footer === true) {
const footerjs = await getPlugin(footerData.customfooterjs);
await footerjs.footerClickListener(e, footerData, modal);
}
};
modal.getBodyPromise()
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
.then(body => body[0])
// Set up the carousel.
.then(body => {
$(body.querySelector(selectors.regions.carousel))
.carousel({
interval: false,
pause: true,
keyboard: false
});
return body;
})
// Add the listener for clicks on the body.
.then(body => {
body.addEventListener('click', bodyClickListener);
return body;
})
// Add a listener for an input change in the activity chooser's search bar.
.then(body => {
const searchInput = body.querySelector(selectors.actions.search);
// The search input is triggered.
searchInput.addEventListener('input', debounce(() => {
// Display the search results.
toggleSearchResultsView(modal, mappedModules, searchInput.value);
}, 300));
return body;
})
// Register event listeners related to the keyboard navigation controls.
.then(body => {
// Get the active chooser options section.
const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute("href");
const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
return body;
})
.catch();
modal.getFooterPromise()
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
.then(footer => footer[0])
// Add the listener for clicks on the footer.
.then(footer => {
footer.addEventListener('click', footerClickListener);
return footer;
})
.catch();
};
/**
* Initialise the keyboard navigation controls for the chooser options.
*
* @method initChooserOptionsKeyboardNavigation
* @param {HTMLElement} body Our modal that we are working with
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items
* @param {Object} modal Our created modal for the section
*/
const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {
const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
Array.from(chooserOptions).forEach((element) => {
return element.addEventListener('keydown', (e) => {
// Check for enter/ space triggers for showing the help.
if (e.keyCode === enter || e.keyCode === space) {
if (e.target.matches(selectors.actions.optionActions.showSummary)) {
e.preventDefault();
const module = e.target.closest(selectors.regions.chooserOption.container);
const moduleName = module.dataset.modname;
const moduleData = mappedModules.get(moduleName);
const carousel = $(body.querySelector(selectors.regions.carousel));
carousel.carousel({
interval: false,
pause: true,
keyboard: false
});
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
moduleData.showFooter = modal.hasFooterContent();
showModuleHelp(carousel, moduleData, modal);
}
}
// Next.
if (e.keyCode === arrowRight) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const nextOption = currentOption.nextElementSibling;
const firstOption = chooserOptionsContainer.firstElementChild;
const toFocusOption = clickErrorHandler(nextOption, firstOption);
focusChooserOption(toFocusOption, currentOption);
}
// Previous.
if (e.keyCode === arrowLeft) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const previousOption = currentOption.previousElementSibling;
const lastOption = chooserOptionsContainer.lastElementChild;
const toFocusOption = clickErrorHandler(previousOption, lastOption);
focusChooserOption(toFocusOption, currentOption);
}
if (e.keyCode === home) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const firstOption = chooserOptionsContainer.firstElementChild;
focusChooserOption(firstOption, currentOption);
}
if (e.keyCode === end) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const lastOption = chooserOptionsContainer.lastElementChild;
focusChooserOption(lastOption, currentOption);
}
});
});
};
/**
* Focus on a chooser option element and remove the previous chooser element from the focus order
*
* @method focusChooserOption
* @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
* @param {HTMLElement|null} previousChooserOption The previous focused option element
*/
const focusChooserOption = (currentChooserOption, previousChooserOption = null) => {
if (previousChooserOption !== null) {
toggleFocusableChooserOption(previousChooserOption, false);
}
toggleFocusableChooserOption(currentChooserOption, true);
currentChooserOption.focus();
};
/**
* Add or remove a chooser option from the focus order.
*
* @method toggleFocusableChooserOption
* @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order
* @param {Boolean} isFocusable Whether the chooser element is focusable or not
*/
const toggleFocusableChooserOption = (chooserOption, isFocusable) => {
const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);
const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);
const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);
if (isFocusable) {
// Set tabindex to 0 to add current chooser option element to the focus order.
chooserOption.tabIndex = 0;
chooserOptionLink.tabIndex = 0;
chooserOptionHelp.tabIndex = 0;
chooserOptionFavourite.tabIndex = 0;
} else {
// Set tabindex to -1 to remove the previous chooser option element from the focus order.
chooserOption.tabIndex = -1;
chooserOptionLink.tabIndex = -1;
chooserOptionHelp.tabIndex = -1;
chooserOptionFavourite.tabIndex = -1;
}
};
/**
* Small error handling function to make sure the navigated to object exists
*
* @method clickErrorHandler
* @param {HTMLElement} item What we want to check exists
* @param {HTMLElement} fallback If we dont match anything fallback the focus
* @return {HTMLElement}
*/
const clickErrorHandler = (item, fallback) => {
if (item !== null) {
return item;
} else {
return fallback;
}
};
/**
* Render the search results in a defined container
*
* @method renderSearchResults
* @param {HTMLElement} searchResultsContainer The container where the data should be rendered
* @param {Object} searchResultsData Data containing the module items that satisfy the search criteria
*/
const renderSearchResults = async(searchResultsContainer, searchResultsData) => {
const templateData = {
'searchresultsnumber': searchResultsData.length,
'searchresults': searchResultsData
};
// Build up the html & js ready to place into the help section.
const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);
await Templates.replaceNodeContents(searchResultsContainer, html, js);
};
/**
* Toggle (display/hide) the search results depending on the value of the search query
*
* @method toggleSearchResultsView
* @param {Object} modal Our created modal for the section
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {String} searchQuery The search query
*/
const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
const modalBody = modal.getBody()[0];
const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);
if (searchQuery.length > 0) { // Search query is present.
const searchResultsData = searchModules(mappedModules, searchQuery);
await renderSearchResults(searchResultsContainer, searchResultsData);
const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);
const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);
if (firstSearchResultItem) {
// Set the first result item to be focusable.
toggleFocusableChooserOption(firstSearchResultItem, true);
// Register keyboard events on the created search result items.
initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
}
// Display the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.remove('d-none');
// Hide the default chooser options container.
chooserContainer.setAttribute('hidden', 'hidden');
// Display the search results container.
searchResultsContainer.removeAttribute('hidden');
} else { // Search query is not present.
// Hide the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.add('d-none');
// Hide the search results container.
searchResultsContainer.setAttribute('hidden', 'hidden');
// Display the default chooser options container.
chooserContainer.removeAttribute('hidden');
}
};
/**
* Return the list of modules which have a name or description that matches the given search term.
*
* @method searchModules
* @param {Array} modules List of available modules
* @param {String} searchTerm The search term to match
* @return {Array}
*/
const searchModules = (modules, searchTerm) => {
if (searchTerm === '') {
return modules;
}
searchTerm = searchTerm.toLowerCase();
const searchResults = [];
modules.forEach((activity) => {
const activityName = activity.title.toLowerCase();
const activityDesc = activity.help.toLowerCase();
if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {
searchResults.push(activity);
}
});
return searchResults;
};
/**
* Set up our tabindex information across the chooser.
*
* @method setupKeyboardAccessibility
* @param {Promise} modal Our created modal for the section
* @param {Map} mappedModules A map of all of the built module information
*/
const setupKeyboardAccessibility = (modal, mappedModules) => {
modal.getModal()[0].tabIndex = -1;
modal.getBodyPromise().then(body => {
$(selectors.elements.tab).on('shown.bs.tab', (e) => {
const activeSectionId = e.target.getAttribute("href");
const activeSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = activeSectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
const prevActiveSectionId = e.relatedTarget.getAttribute("href");
const prevActiveSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
// Disable the focus of every chooser option in the previous active section.
disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
// Enable the focus of the first chooser option in the current active section.
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);
});
return;
}).catch(Notification.exception);
};
/**
* Disable the focus of all chooser options in a specific container (section).
*
* @method disableFocusAllChooserOptions
* @param {HTMLElement} sectionChooserOptions The section that contains the chooser items
*/
const disableFocusAllChooserOptions = (sectionChooserOptions) => {
const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);
allChooserOptions.forEach((chooserOption) => {
toggleFocusableChooserOption(chooserOption, false);
});
};
/**
* Display the module chooser.
*
* @method displayChooser
* @param {Promise} modalPromise Our created modal for the section
* @param {Array} sectionModules An array of all of the built module information
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
* @param {Object} footerData Our base footer object.
*/
export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {
// Make a map so we can quickly fetch a specific module's object for either rendering or searching.
const mappedModules = new Map();
sectionModules.forEach((module) => {
mappedModules.set(module.componentname + '_' + module.link, module);
});
// Register event listeners.
modalPromise.then(modal => {
registerListenerEvents(modal, mappedModules, partialFavourite, footerData);
// We want to focus on the first chooser option element as soon as the modal is opened.
setupKeyboardAccessibility(modal, mappedModules);
// We want to focus on the action select when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
return modal;
}).catch();
};
@@ -0,0 +1,99 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle user AJAX actions.
*
* @module core_course/local/activitychooser/repository
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Fetch all the information on modules we'll need in the activity chooser.
*
* @method activityModules
* @param {Number} courseid What course to fetch the modules for
* @return {object} jQuery promise
*/
export const activityModules = (courseid) => {
const request = {
methodname: 'core_course_get_course_content_items',
args: {
courseid: courseid,
},
};
return ajax.call([request])[0];
};
/**
* Given a module name, module ID & the current course we want to specify that the module
* is a users' favourite.
*
* @method favouriteModule
* @param {String} modName Frankenstyle name of the component to add favourite
* @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
* @return {object} jQuery promise
*/
export const favouriteModule = (modName, modID) => {
const request = {
methodname: 'core_course_add_content_item_to_user_favourites',
args: {
componentname: modName,
contentitemid: modID,
},
};
return ajax.call([request])[0];
};
/**
* Given a module name, module ID & the current course we want to specify that the module
* is no longer a users' favourite.
*
* @method unfavouriteModule
* @param {String} modName Frankenstyle name of the component to add favourite
* @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
* @return {object} jQuery promise
*/
export const unfavouriteModule = (modName, modID) => {
const request = {
methodname: 'core_course_remove_content_item_from_user_favourites',
args: {
componentname: modName,
contentitemid: modID,
},
};
return ajax.call([request])[0];
};
/**
* Fetch all the information on modules we'll need in the activity chooser.
*
* @method fetchFooterData
* @param {Number} courseid What course to fetch the data for
* @param {Number} sectionid What section to fetch the data for
* @return {object} jQuery promise
*/
export const fetchFooterData = (courseid, sectionid) => {
const request = {
methodname: 'core_course_get_activity_chooser_footer',
args: {
courseid: courseid,
sectionid: sectionid,
},
};
return ajax.call([request])[0];
};
@@ -0,0 +1,88 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module core_course/local/activitychooser/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* A small helper function to build queryable data selectors.
* @method getDataSelector
* @param {String} name
* @param {String} value
* @return {string}
*/
const getDataSelector = (name, value) => {
return `[data-${name}="${value}"]`;
};
export default {
regions: {
chooser: getDataSelector('region', 'chooser-container'),
getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,
chooserOption: {
container: getDataSelector('region', 'chooser-option-container'),
actions: getDataSelector('region', 'chooser-option-actions-container'),
info: getDataSelector('region', 'chooser-option-info-container'),
},
chooserSummary: {
container: getDataSelector('region', 'chooser-option-summary-container'),
content: getDataSelector('region', 'chooser-option-summary-content-container'),
header: getDataSelector('region', 'summary-header'),
actions: getDataSelector('region', 'chooser-option-summary-actions-container'),
},
carousel: getDataSelector('region', 'carousel'),
help: getDataSelector('region', 'help'),
modules: getDataSelector('region', 'modules'),
favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
defaultTabNav: getDataSelector('region', 'default-tab-nav'),
activityTabNav: getDataSelector('region', 'activity-tab-nav'),
favouriteTab: getDataSelector('region', 'favourites'),
recommendedTab: getDataSelector('region', 'recommended'),
defaultTab: getDataSelector('region', 'default'),
activityTab: getDataSelector('region', 'activity'),
resourceTab: getDataSelector('region', 'resources'),
getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`,
searchResults: getDataSelector('region', 'search-results-container'),
searchResultItems: getDataSelector('region', 'search-result-items-container'),
},
actions: {
optionActions: {
showSummary: getDataSelector('action', 'show-option-summary'),
manageFavourite: getDataSelector('action', 'manage-module-favourite'),
},
addChooser: getDataSelector('action', 'add-chooser-option'),
closeOption: getDataSelector('action', 'close-chooser-option-summary'),
hide: getDataSelector('action', 'hide'),
search: getDataSelector('action', 'search'),
clearSearch: getDataSelector('action', 'clearsearch'),
},
render: {
favourites: getDataSelector('render', 'favourites-area'),
},
elements: {
section: '.section',
sectionmodchooser: 'button.section-modchooser-link',
sitemenu: '.block_site_main_menu',
sitetopic: 'div.sitetopic',
tab: 'a[data-toggle="tab"]',
activetab: 'a[data-toggle="tab"][aria-selected="true"]',
visibletabs: 'a[data-toggle="tab"]:not(.d-none)'
},
};
+143
View File
@@ -0,0 +1,143 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the functionality for toggling the manual completion state of a course module through
* the manual completion button.
*
* @module core_course/manual_completion_toggle
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Notification from 'core/notification';
import {toggleManualCompletion} from 'core_course/repository';
import * as CourseEvents from 'core_course/events';
import Pending from 'core/pending';
/**
* Selectors in the manual completion template.
*
* @type {{MANUAL_TOGGLE: string}}
*/
const SELECTORS = {
MANUAL_TOGGLE: 'button[data-action=toggle-manual-completion]',
};
/**
* Toggle type values for the data-toggletype attribute in the core_course/completion_manual template.
*
* @type {{TOGGLE_UNDO: string, TOGGLE_MARK_DONE: string}}
*/
const TOGGLE_TYPES = {
TOGGLE_MARK_DONE: 'manual:mark-done',
TOGGLE_UNDO: 'manual:undo',
};
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
/**
* Registers the click event listener for the manual completion toggle button.
*/
export const init = () => {
if (registered) {
return;
}
document.addEventListener('click', (e) => {
const toggleButton = e.target.closest(SELECTORS.MANUAL_TOGGLE);
if (toggleButton) {
e.preventDefault();
toggleManualCompletionState(toggleButton).catch(Notification.exception);
}
});
registered = true;
};
/**
* Toggles the manual completion state of the module for the given user.
*
* @param {HTMLElement} toggleButton
* @returns {Promise<void>}
*/
const toggleManualCompletionState = async(toggleButton) => {
const pendingPromise = new Pending('core_course:toggleManualCompletionState');
// Make a copy of the original content of the button.
const originalInnerHtml = toggleButton.innerHTML;
// Disable the button to prevent double clicks.
toggleButton.setAttribute('disabled', 'disabled');
// Get button data.
const toggleType = toggleButton.getAttribute('data-toggletype');
const cmid = toggleButton.getAttribute('data-cmid');
const activityname = toggleButton.getAttribute('data-activityname');
// Get the target completion state.
const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE;
// Replace the button contents with the loading icon.
Templates.renderForPromise('core/loading', {})
.then((loadingHtml) => {
Templates.replaceNodeContents(toggleButton, loadingHtml, '');
return;
}).catch(() => {});
try {
// Call the webservice to update the manual completion status.
await toggleManualCompletion(cmid, completed);
// All good so far. Refresh the manual completion button to reflect its new state by re-rendering the template.
const templateContext = {
cmid: cmid,
activityname: activityname,
overallcomplete: completed,
overallincomplete: !completed,
istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.
};
const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);
// Replace the toggle button with the newly loaded template.
const replacedNode = await Templates.replaceNode(toggleButton, renderObject.html, renderObject.js);
const newToggleButton = replacedNode.pop();
// Build manualCompletionToggled custom event.
const withAvailability = toggleButton.getAttribute('data-withavailability');
const toggledEvent = new CustomEvent(CourseEvents.manualCompletionToggled, {
bubbles: true,
detail: {
cmid,
activityname,
completed,
withAvailability,
}
});
// Dispatch the manualCompletionToggled custom event.
newToggleButton.dispatchEvent(toggledEvent);
} catch (exception) {
// In case of an error, revert the original state and appearance of the button.
toggleButton.removeAttribute('disabled');
toggleButton.innerHTML = originalInnerHtml;
// Show the exception.
Notification.exception(exception);
}
pendingPromise.resolve();
};
+54
View File
@@ -0,0 +1,54 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle toggling activity chooser recommendations.
*
* @module core_course/recommendations
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Notification from 'core/notification';
/**
* Do an ajax call to toggle the recommendation
*
* @param {object} e The event
* @return {void}
*/
const toggleRecommendation = (e) => {
let data = {
methodname: 'core_course_toggle_activity_recommendation',
args: {
area: e.currentTarget.dataset.area,
id: e.currentTarget.dataset.id
}
};
Ajax.call([data])[0].fail(Notification.exception);
};
/**
* Initialisation function
*
* @return {void}
*/
export const init = () => {
const checkboxelements = document.querySelectorAll("[data-area]");
checkboxelements.forEach((checkbox) => {
checkbox.addEventListener('change', toggleRecommendation);
});
};
+199
View File
@@ -0,0 +1,199 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle course ajax actions.
*
* @module core_course/repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Get the list of courses that the logged in user is enrolled in for a given
* timeline classification.
*
* @param {string} classification past, inprogress, or future
* @param {int} limit Only return this many results
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @return {object} jQuery promise resolved with courses.
*/
const getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {
const args = {
classification: classification
};
if (typeof limit !== 'undefined') {
args.limit = limit;
}
if (typeof offset !== 'undefined') {
args.offset = offset;
}
if (typeof sort !== 'undefined') {
args.sort = sort;
}
const request = {
methodname: 'core_course_get_enrolled_courses_by_timeline_classification',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get a list of courses that the logged in user is enrolled in, where they have at least one action event,
* for a given timeline classification.
*
* @param {string} classification past, inprogress, or future
* @param {int} limit The maximum number of courses to return
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @param {string} searchValue Optional text search value
* @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in
* @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in
* @return {object} jQuery promise resolved with courses.
*/
const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null,
searchValue = null, eventsFrom = null, eventsTo = null) => {
const args = {
classification: classification,
limit: limit,
offset: offset,
sort: sort,
eventsfrom: eventsFrom,
eventsto: eventsTo,
searchvalue: searchValue,
};
const request = {
methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get the list of courses that the user has most recently accessed.
*
* @method getLastAccessedCourses
* @param {int} userid User from which the courses will be obtained
* @param {int} limit Only return this many results
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @return {promise} Resolved with an array of courses
*/
const getLastAccessedCourses = (userid, limit, offset, sort) => {
const args = {};
if (typeof userid !== 'undefined') {
args.userid = userid;
}
if (typeof limit !== 'undefined') {
args.limit = limit;
}
if (typeof offset !== 'undefined') {
args.offset = offset;
}
if (typeof sort !== 'undefined') {
args.sort = sort;
}
const request = {
methodname: 'core_course_get_recent_courses',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get the list of users enrolled in this cmid.
*
* @param {Number} cmid Course Module from which the users will be obtained
* @param {Number} groupID Group ID from which the users will be obtained
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
* @returns {Promise} Promise containing a list of users
*/
const getEnrolledUsersFromCourseModuleID = (cmid, groupID, onlyActive = false) => {
var request = {
methodname: 'core_course_get_enrolled_users_by_cmid',
args: {
cmid: cmid,
groupid: groupID,
onlyactive: onlyActive,
},
};
return Ajax.call([request])[0];
};
/**
* Get the list of gradable users enrolled in this course.
*
* @param {Number} courseid Course ID from which the users will be obtained
* @param {Number} groupID Group ID from which the users will be obtained
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
* @returns {Promise} Promise containing a list of users
*/
const getGradabaleUsersFromCourseID = (courseid, groupID, onlyActive = false) => {
const request = {
methodname: 'core_grades_get_gradable_users',
args: {
courseid: courseid,
groupid: groupID,
onlyactive: onlyActive,
},
};
return Ajax.call([request])[0];
};
/**
* Toggle the completion state of an activity with manual completion.
*
* @param {Number} cmid The course module ID.
* @param {Boolean} completed Whether to set as complete or not.
* @returns {object} jQuery promise
*/
const toggleManualCompletion = (cmid, completed) => {
const request = {
methodname: 'core_completion_update_activity_completion_status_manually',
args: {
cmid,
completed,
}
};
return Ajax.call([request])[0];
};
export default {
getEnrolledCoursesByTimelineClassification,
getLastAccessedCourses,
getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,
getGradableUsersFromCourseID: getGradabaleUsersFromCourseID,
toggleManualCompletion,
getEnrolledCoursesWithEventsByTimelineClassification,
};
+49
View File
@@ -0,0 +1,49 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* JS module for the course homepage.
*
* @module core_course/view
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as CourseEvents from 'core_course/events';
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
/**
* Function to intialise and register event listeners for this module.
*/
export const init = () => {
if (registered) {
return;
}
// Listen for toggled manual completion states of activities.
document.addEventListener(CourseEvents.manualCompletionToggled, (e) => {
const withAvailability = parseInt(e.detail.withAvailability);
if (withAvailability) {
// Reload the page when the toggled manual completion button has availability conditions linked to it.
window.location.reload();
}
});
registered = true;
};
+80
View File
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Bulk activity completion selection
*
* @package core_completion
* @category completion
* @copyright 2017 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../config.php');
require_once($CFG->dirroot.'/course/lib.php');
require_once($CFG->libdir.'/completionlib.php');
$id = required_param('id', PARAM_INT); // Course id.
$cmids = optional_param_array('cmid', [], PARAM_INT);
// Perform some basic access control checks.
if ($id) {
if ($id == SITEID) {
// Don't allow editing of 'site course' using this form.
throw new \moodle_exception('cannoteditsiteform');
}
if (!$course = $DB->get_record('course', array('id' => $id))) {
throw new \moodle_exception('invalidcourseid');
}
require_login($course);
} else {
require_login();
throw new \moodle_exception('needcourseid');
}
// Set up the page.
navigation_node::override_active_url(new moodle_url('/course/completion.php', array('id' => $course->id)));
$PAGE->set_course($course);
$PAGE->set_url('/course/bulkcompletion.php', array('id' => $course->id));
$PAGE->set_title($course->shortname);
$PAGE->set_heading($course->fullname);
$PAGE->set_pagelayout('admin');
// Check access.
if (!core_completion\manager::can_edit_bulk_completion($id)) {
require_capability('moodle/course:manageactivities', context_course::instance($course->id));
}
// Get all that stuff I need for the renderer.
$manager = new \core_completion\manager($id);
$bulkcompletiondata = $manager->get_activities_and_headings();
$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
// Print the form.
echo $OUTPUT->header();
$actionbar = new \core_course\output\completion_action_bar($course->id, $PAGE->url);
echo $renderer->render_course_completion_action_bar($actionbar);
$PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['theform']);
echo $renderer->bulkcompletion($bulkcompletiondata);
echo $OUTPUT->footer();
+39
View File
@@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Helps moodle-course-categoryexpander to serve AJAX requests
*
* @see core_course_renderer::coursecat_include_js()
* @see core_course_renderer::coursecat_ajax()
*
* @package core
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require_once(__DIR__ . '/../config.php');
if ($CFG->forcelogin) {
require_login();
}
$PAGE->set_context(context_system::instance());
$courserenderer = $PAGE->get_renderer('core', 'course');
echo json_encode($courserenderer->coursecat_ajax());
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This script allows the number of sections in a course to be increased
* or decreased, redirecting to the course page.
*
* @package core_course
* @copyright 2012 Dan Poltawski
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.3
*/
require_once(__DIR__.'/../config.php');
require_once($CFG->dirroot.'/course/lib.php');
$courseid = required_param('courseid', PARAM_INT);
$increase = optional_param('increase', null, PARAM_BOOL);
$insertsection = optional_param('insertsection', null, PARAM_INT); // Insert section at position; 0 means at the end.
$numsections = optional_param('numsections', 1, PARAM_INT); // Number of sections to insert.
$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); // Where to return to after the action.
$sectionreturn = optional_param('sectionreturn', null, PARAM_INT); // Section to return to, ignored if $returnurl is specified.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courseformatoptions = course_get_format($course)->get_format_options();
$PAGE->set_url('/course/changenumsections.php', array('courseid' => $courseid));
// Authorisation checks.
require_login($course);
require_capability('moodle/course:update', context_course::instance($course->id));
require_sesskey();
$desirednumsections = 0;
$courseformat = course_get_format($course);
$lastsectionnumber = $courseformat->get_last_section_number();
$maxsections = $courseformat->get_max_sections();
if (isset($courseformatoptions['numsections']) && $increase !== null) {
$desirednumsections = $courseformatoptions['numsections'] + 1;
} else if (course_get_format($course)->uses_sections() && $insertsection !== null) {
// Count the sections in the course.
$desirednumsections = $lastsectionnumber + $numsections;
}
if ($desirednumsections > $maxsections) {
// Increase in number of sections is not allowed.
\core\notification::warning(get_string('maxsectionslimit', 'moodle', $maxsections));
$increase = null;
$insertsection = null;
$numsections = 0;
if (!$returnurl) {
$returnurl = course_get_url($course);
}
}
if (isset($courseformatoptions['numsections']) && $increase !== null) {
if ($increase) {
// Add an additional section.
$courseformatoptions['numsections']++;
course_create_sections_if_missing($course, $courseformatoptions['numsections']);
} else {
// Remove a section.
$courseformatoptions['numsections']--;
}
// Don't go less than 0, intentionally redirect silently (for the case of
// double clicks).
if ($courseformatoptions['numsections'] >= 0) {
update_course((object)array('id' => $course->id,
'numsections' => $courseformatoptions['numsections']));
}
if (!$returnurl) {
$returnurl = course_get_url($course);
$returnurl->set_anchor('changenumsections');
}
} else if (course_get_format($course)->uses_sections() && $insertsection !== null) {
if ($insertsection) {
// Inserting sections at any position except in the very end requires capability to move sections.
require_capability('moodle/course:movesections', context_course::instance($course->id));
}
$sections = [];
for ($i = 0; $i < max($numsections, 1); $i ++) {
$sections[] = course_create_section($course, $insertsection);
}
if (!$returnurl) {
$returnurl = course_get_url($course, $sections[0]->section,
($sectionreturn !== null) ? ['sr' => $sectionreturn] : []);
}
}
// Redirect to where we were..
redirect($returnurl);
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/calendar/externallib.php');
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activities_due extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:activitiesdue');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
$user = $this->retrieve('user', $sampleid);
$actionevents = \core_calendar_external::get_calendar_action_events_by_timesort($starttime, $endtime, 0, 1,
true, $user->id);
$useractionevents = [];
if ($actionevents->events) {
// We first need to check that at least one of the core_calendar_provide_event_action
// callbacks has the $userid param.
foreach ($actionevents->events as $event) {
$nparams = $this->get_provide_event_action_num_params($event->modulename);
if ($nparams > 2) {
// Just the basic info for the insight as we want a low memory usage.
$useractionevents[$event->id] = (object)[
'name' => $event->name,
'url' => $event->url,
'time' => $event->timesort,
'coursename' => $event->course->fullnamedisplay,
'icon' => $event->icon,
];
}
}
if (!empty($useractionevents)) {
$this->add_shared_calculation_info($sampleid, $useractionevents);
return self::get_max_value();
}
}
return self::get_min_value();
}
/**
* Returns the number of params declared in core_calendar_provide_event_action's implementation.
*
* @param string $modulename The module name
* @return int
*/
private function get_provide_event_action_num_params(string $modulename) {
$functionname = 'mod_' . $modulename . '_core_calendar_provide_event_action';
$reflection = new \ReflectionFunction($functionname);
return $reflection->getNumberOfParameters();
}
}
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Completion enabled set indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/completionlib.php');
/**
* Completion enabled set indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_enabled extends \core_analytics\local\indicator\binary {
/**
* get_name
*
* @return new \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:completionenabled', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// Minimum course although it also accepts course_modules.
return array('course');
}
/**
* Is completion enabled? Work both with courses and activities.
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$course = $this->retrieve('course', $sampleid);
// It may not be available, but if it is the indicator checks if completion is enabled for the cm.
$cm = $this->retrieve('course_modules', $sampleid);
$completion = new \completion_info($course);
if (!$completion->is_enabled($cm)) {
$value = self::get_min_value();
} else if (!$cm && !$completion->has_criteria()) {
// Course completion enabled with no criteria counts as nothing.
$value = self::get_min_value();
} else {
$value = self::get_max_value();
}
return $value;
}
}
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* No student indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* No student indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_student extends \core_analytics\local\indicator\binary {
/**
* Student role ids.
*
* @var array|null
*/
protected $studentroleids = null;
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:nostudent', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although calculate_sample only reads context, we need the context to be course
// or below.
return array('context', 'course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$context = $this->retrieve('context', $sampleid);
if (is_null($this->studentroleids)) {
$this->studentroleids = array_keys(get_archetype_roles('student'));
}
foreach ($this->studentroleids as $role) {
// We look for roles, not enrolments as a student assigned at category level is supposed to be a
// course student.
$students = get_role_users($role, $context, false, 'u.id', 'u.id');
if ($students) {
return self::get_max_value();
}
}
return self::get_min_value();
}
}
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* No teacher indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* No teacher indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_teacher extends \core_analytics\local\indicator\binary {
/**
* Teacher role ids.
*
* @var array|null
*/
protected $teacherroleids = null;
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:noteacher', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although calculate_sample only reads context, we need the context to be course
// or below.
return array('context', 'course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$context = $this->retrieve('context', $sampleid);
if (is_null($this->teacherroleids)) {
$this->teacherroleids = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
}
foreach ($this->teacherroleids as $role) {
// We look for roles, not enrolments as a teacher assigned at category level is supposed to be a
// course teacher.
$teachers = get_role_users($role, $context, false, 'u.id', 'u.id');
if ($teachers) {
return self::get_max_value();
}
}
return self::get_min_value();
}
}
@@ -0,0 +1,136 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Potential cognitive depth indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
use \core_analytics\local\indicator\community_of_inquiry_activity;
/**
* Potential cognitive depth indicator.
*
* It extends linear instead of discrete as there is a linear relation between
* the different cognitive levels activities can reach.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class potential_cognitive_depth extends \core_analytics\local\indicator\linear {
/**
* get_name
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:potentialcognitive', 'moodle');
}
/**
* Specify the required data to process this indicator.
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although this indicator can also work with course_modules we can't
// calculate anything without the course.
return array('course');
}
/**
* calculate_sample
*
* @throws \coding_exception
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
if ($sampleorigin === 'course_modules') {
$cm = $this->retrieve('course_modules', $sampleid);
$cminfo = \cm_info::create($cm);
$cognitivedepthindicator = $this->get_cognitive_indicator($cminfo->modname);
$potentiallevel = $cognitivedepthindicator->get_cognitive_depth_level($cminfo);
if ($potentiallevel > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
throw new \coding_exception('Maximum cognitive depth level is ' .
community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $potentiallevel . ' provided by ' .
get_class($this));
}
} else {
$course = $this->retrieve('course', $sampleid);
$modinfo = get_fast_modinfo($course);
$cms = $modinfo->get_cms();
if (!$cms) {
return self::get_min_value();
}
$potentiallevel = 0;
foreach ($cms as $cm) {
if (!$cognitivedepthindicator = $this->get_cognitive_indicator($cm->modname)) {
continue;
}
$level = $cognitivedepthindicator->get_cognitive_depth_level($cm);
if ($level > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
throw new \coding_exception('Maximum cognitive depth level is ' .
community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $level . ' provided by ' . get_class($this));
}
if ($level > $potentiallevel) {
$potentiallevel = $level;
}
}
}
// Values from -1 to 1 range split in 5 parts (the max cognitive depth level).
// Note that we divide by 4 because we start from -1.
$levelscore = round((self::get_max_value() - self::get_min_value()) / 4, 2);
// We substract $levelscore because we want to start from the lower score and there is no cognitive depth level 0.
return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
}
/**
* Returns the cognitive depth class of this indicator.
*
* @param string $modname
* @return \core_analytics\local\indicator\base|false
*/
protected function get_cognitive_indicator($modname) {
$indicators = \core_analytics\manager::get_all_indicators();
foreach ($indicators as $indicator) {
if ($indicator instanceof community_of_inquiry_activity &&
$indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_COGNITIVE &&
$indicator->get_activity_type() === $modname) {
return $indicator;
}
}
return false;
}
}
@@ -0,0 +1,148 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Potential social breadth indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
use \core_analytics\local\indicator\community_of_inquiry_activity;
/**
* Potential social breadth indicator.
*
* It extends linear instead of discrete as there is a linear relation between
* the different social levels activities can reach.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class potential_social_breadth extends \core_analytics\local\indicator\linear {
/**
* get_name
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:potentialsocial', 'moodle');
}
/**
* Specify the required data to process this indicator.
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although this indicator can also work with course_modules we can't
// calculate anything without the course.
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
if ($sampleorigin === 'course_modules') {
$cm = $this->retrieve('course_modules', $sampleid);
$cminfo = \cm_info::create($cm);
$socialbreadthindicator = $this->get_social_indicator($cminfo->modname);
$potentiallevel = $socialbreadthindicator->get_social_breadth_level($cminfo);
if ($potentiallevel > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
$this->level_not_accepted($potentiallevel);
}
} else {
$course = $this->retrieve('course', $sampleid);
$modinfo = get_fast_modinfo($course);
$cms = $modinfo->get_cms();
if (!$cms) {
return self::get_min_value();
}
$potentiallevel = 0;
foreach ($cms as $cm) {
if (!$socialbreadthindicator = $this->get_social_indicator($cm->modname)) {
continue;
}
$level = $socialbreadthindicator->get_social_breadth_level($cm);
if ($level > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
$this->level_not_accepted($level);
}
if ($level > $potentiallevel) {
$potentiallevel = $level;
}
}
}
// Core activities social breadth only reaches level 2, until core activities social
// breadth do not reach level 5 we limit it to what we currently support, which is level 2.
if ($potentiallevel > 2) {
$potentiallevel = 2;
}
// Supporting only social breadth level 1 and 2 the possible values are -1 or 1.
$levelscore = round(self::get_max_value() - self::get_min_value(), 2);
// We substract $levelscore because we want to start from the lower socre and there is no cognitive depth level 0.
return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
}
/**
* Returns the social breadth class of this indicator.
*
* @param string $modname
* @return \core_analytics\local\indicator\base|false
*/
protected function get_social_indicator($modname) {
$indicators = \core_analytics\manager::get_all_indicators();
foreach ($indicators as $indicator) {
if ($indicator instanceof community_of_inquiry_activity &&
$indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_SOCIAL &&
$indicator->get_activity_type() === $modname) {
return $indicator;
}
}
return false;
}
/**
* Throw a \coding_exception.
*
* @param int $level
*/
protected function level_not_accepted($level) {
throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
}
}
@@ -0,0 +1,141 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course competencies achievement target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Course competencies achievement target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_competencies extends course_enrolments {
/**
* Number of competencies assigned per course.
* @var int[]
*/
protected $coursecompetencies = array();
/**
* Count the competencies in a course.
*
* Save the value in $coursecompetencies array to prevent new accesses to the database.
*
* @param int $courseid The course id.
* @return int Number of competencies assigned to the course.
*/
protected function get_num_competencies_in_course($courseid) {
if (!isset($this->coursecompetencies[$courseid])) {
$ccs = \core_competency\api::count_competencies_in_course($courseid);
// Save the number of competencies per course to avoid another database access in calculate_sample().
$this->coursecompetencies[$courseid] = $ccs;
} else {
$ccs = $this->coursecompetencies[$courseid];
}
return $ccs;
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursecompetencies', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentcompetenciesno', 'course'),
get_string('targetlabelstudentcompetenciesyes', 'course'),
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
$ccs = $this->get_num_competencies_in_course($course->get_id());
if (!$ccs) {
return get_string('nocompetenciesincourse', 'tool_lp');
}
return true;
}
/**
* To have the proficiency or not in each of the competencies assigned to the course sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> competencies achieved, 1 -> competencies not achieved
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
$key = $course->get_id();
// Number of competencies in the course.
$ccs = $this->get_num_competencies_in_course($key);
// Number of proficient competencies in the same course for the user.
$ucs = \core_competency\api::count_proficient_competencies_in_course_for_user($key, $userenrol->userid);
// If they are the equals, the user achieved all the competencies assigned to the course.
if ($ccs == $ucs) {
return 0;
}
return 1;
}
}
@@ -0,0 +1,114 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course completion target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->dirroot . '/completion/completion_completion.php');
/**
* Course completion target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_completion extends course_enrolments {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursecompletion', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentcompletionno', 'course'),
get_string('targetlabelstudentcompletionyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
// Not a valid target if completion is not enabled or there are not completion criteria defined.
$completion = new \completion_info($course->get_course_data());
if (!$completion->is_enabled() || !$completion->has_criteria()) {
return get_string('completionnotenabledforcourse', 'completion');
}
return true;
}
/**
* Course completion sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> course not completed, 1 -> course completed
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// We use completion as a success metric.
$ccompletion = new \completion_completion(array('userid' => $userenrol->userid, 'course' => $course->get_id()));
if ($ccompletion->is_complete()) {
return 0;
} else {
return 1;
}
}
}
@@ -0,0 +1,154 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drop out course target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/lib/gradelib.php');
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->dirroot . '/completion/completion_completion.php');
/**
* Drop out course target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_dropout extends course_enrolments {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursedropout', 'course');
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentdropoutno', 'course'),
get_string('targetlabelstudentdropoutyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
global $DB;
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
if ($fortraining) {
// Not a valid target for training if there are not enough course accesses between the course start and end dates.
$params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(),
'end' => $course->get_end());
list($studentssql, $studentparams) = $DB->get_in_or_equal($this->students, SQL_PARAMS_NAMED);
// Using anonymous to use the db index, not filtering by timecreated to speed it up.
$select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' .
'AND userid ' . $studentssql;
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
$nlogs = $logstore->get_events_select_count($select, array_merge($params, $studentparams));
// At least a minimum of students activity.
$nstudents = count($this->students);
if ($nlogs / $nstudents < 10) {
return get_string('nocourseactivity', 'course');
}
}
return true;
}
/**
* calculate_sample
*
* The meaning of a drop out changes depending on the settings enabled in the course. Following these priorities order:
* 1.- Course completion
* 2.- No logs during the last quarter of the course
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> not at risk, 1 -> at risk
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// We use completion as a success metric only when it is enabled.
$completion = new \completion_info($course->get_course_data());
if ($completion->is_enabled() && $completion->has_criteria()) {
$ccompletion = new \completion_completion(array('userid' => $userenrol->userid, 'course' => $course->get_id()));
if ($ccompletion->is_complete()) {
return 0;
} else {
return 1;
}
}
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// No logs during the last quarter of the course.
$courseduration = $course->get_end() - $course->get_start();
$limit = intval($course->get_end() - ($courseduration / 4));
$select = "courseid = :courseid AND userid = :userid AND timecreated > :limit";
$params = array('userid' => $userenrol->userid, 'courseid' => $course->get_id(), 'limit' => $limit);
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs == 0) {
return 1;
}
return 0;
}
}
@@ -0,0 +1,393 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for targets whose analysable is a course using user enrolments as samples.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Base class for targets whose analysable is a course using user enrolments as samples.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class course_enrolments extends \core_analytics\local\target\binary {
/**
* @var string
*/
const MESSAGE_ACTION_NAME = 'studentmessage';
/**
* @var float
*/
const ENROL_ACTIVE_PERCENT_REQUIRED = 0.7;
/**
* Students in the course.
* @var int[]
*/
protected $students;
/**
* Returns the analyser class that should be used along with this target.
*
* @return string The full class name as a string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\student_enrolments';
}
/**
* Only past stuff.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\before_now);
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
public function get_insight_subject(int $modelid, \context $context) {
return get_string('studentsatriskincourse', 'course', $context->get_context_name(false));
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return string[] The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('studentsatriskinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('studentsatriskinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
if (!$course->was_started()) {
return get_string('coursenotyetstarted', 'course');
}
if (!$fortraining && !$course->get_course_data()->visible) {
return get_string('hiddenfromstudents');
}
if (!$this->students = $course->get_students()) {
return get_string('nocoursestudents', 'course');
}
if (!course_format_uses_sections($course->get_course_data()->format)) {
// We can not split activities in time ranges.
return get_string('nocoursesections', 'course');
}
if ($course->get_end() == 0) {
// We require time end to be set.
return get_string('nocourseendtime', 'course');
}
if ($course->get_end() < $course->get_start()) {
return get_string('errorendbeforestart', 'course');
}
// A course that lasts longer than 1 year probably have wrong start or end dates.
if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
return get_string('coursetoolong', 'course');
}
// Finished courses can not be used to get predictions.
if (!$fortraining && $course->is_finished()) {
return get_string('coursealreadyfinished', 'course');
}
if ($fortraining) {
// Ongoing courses data can not be used to train.
if (!$course->is_finished()) {
return get_string('coursenotyetfinished', 'course');
}
}
return true;
}
/**
* Discard student enrolments that are invalid.
*
* Note that this method assumes that the target is only interested in enrolments that are/were active
* between the current course start and end times. Targets interested in predicting students at risk before
* their enrolment start and targets interested in getting predictions for students whose enrolment already
* finished should overwrite this method as these students are discarded by this method.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
$now = time();
$userenrol = $this->retrieve('user_enrolments', $sampleid);
if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
// Discard enrolments which time end is prior to the course start. This should get rid of
// old user enrolments that remain on the course.
return false;
}
$limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
if (($userenrol->timestart && $userenrol->timestart < $limit) ||
(!$userenrol->timestart && $userenrol->timecreated < $limit)) {
// Following what we do in is_valid_analysable, we will discard enrolments that last more than 1 academic year
// because they have incorrect start and end dates or because they are reused along multiple years
// without removing previous academic years students. This may not be very accurate because some courses
// can last just some months, but it is better than nothing.
return false;
}
if ($course->get_end()) {
if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
(!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
// Discard user enrolments that start after the analysable official end.
return false;
}
}
if ($now < $userenrol->timestart && $userenrol->timestart) {
// Discard enrolments whose start date is after now (no need to check timecreated > $now :P).
return false;
}
if (!$fortraining && $userenrol->timeend && $userenrol->timeend < $now) {
// We don't want to generate predictions for finished enrolments.
return false;
}
return true;
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param bool $includedetailsaction
* @param bool $isinsightuser
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
$isinsightuser = false) {
$actions = array();
$sampledata = $prediction->get_sample_data();
$studentid = $sampledata['user']->id;
// View outline report.
$url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
'mode' => 'outline'));
$pix = new \pix_icon('i/report', get_string('outlinereport'));
$actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'), false, ['target' => '_blank']);
return array_merge(parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser), $actions);
}
/**
* Suggested bulk actions for a user.
*
* @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use.
* @return \core_analytics\bulk_action[] The list of bulk actions.
*/
public function bulk_actions(array $predictions) {
$actions = [];
$userids = [];
foreach ($predictions as $prediction) {
$sampledata = $prediction->get_sample_data();
$userid = $sampledata['user']->id;
// Indexed by prediction id because we want the predictionid-userid
// mapping later when sending the message.
$userids[$prediction->get_prediction_data()->id] = $userid;
}
// Send a message for all the students.
$attrs = array(
'data-bulk-sendmessage' => '1',
'data-prediction-to-user-id' => json_encode($userids)
);
$actions[] = new \core_analytics\bulk_action(self::MESSAGE_ACTION_NAME, new \moodle_url(''),
new \pix_icon('t/message', get_string('sendmessage', 'message')),
get_string('sendmessage', 'message'), true, $attrs);
return array_merge($actions, parent::bulk_actions($predictions));
}
/**
* Adds the JS required to run the bulk actions.
*/
public function add_bulk_actions_js() {
global $PAGE;
$PAGE->requires->js_call_amd('report_insights/message_users', 'init',
['.insights-bulk-actions', self::MESSAGE_ACTION_NAME]);
parent::add_bulk_actions_js();
}
/**
* Is/was this user enrolment active during most of the analysis interval?
*
* This method discards enrolments that were not active during most of the analysis interval. It is
* important to discard these enrolments because the indicator calculations can lead to misleading
* results.
*
* Note that this method assumes that the target is interested in enrolments that are/were active
* during the analysis interval. Targets interested in predicting students at risk before
* their enrolment start should not call this method. Similarly, targets interested in getting
* predictions for students whose enrolment already finished should not call this method either.
*
* @param int $sampleid The id of the sample that is being calculated
* @param int $starttime The analysis interval start time
* @param int $endtime The analysis interval end time
* @return bool
*/
protected function enrolment_active_during_analysis_time(int $sampleid, int $starttime, int $endtime) {
$userenrol = $this->retrieve('user_enrolments', $sampleid);
if (!empty($userenrol->timestart)) {
$enrolstart = $userenrol->timestart;
} else {
// This is always set.
$enrolstart = $userenrol->timecreated;
}
if (!empty($userenrol->timeend)) {
$enrolend = $userenrol->timeend;
} else {
// Default to tre end of the world.
$enrolend = PHP_INT_MAX;
}
if ($endtime && $endtime < $enrolstart) {
/* The enrolment starts/ed after the analysis end time.
* |=========| |----------|
* A start A end E start E end
*/
return false;
}
if ($starttime && $enrolend < $starttime) {
/* The enrolment finishes/ed before the analysis start time.
* |---------| |==========|
* E start E end A start A end
*/
return false;
}
// Now we want to discard enrolments that were not active for most of the analysis interval. We
// need both a $starttime and an $endtime to calculate this.
if (!$starttime) {
// Early return. Nothing to discard if there is no start.
return true;
}
if (!$endtime) {
// We can not calculate in relative terms (percent) how far from the analysis start time
// this enrolment start is/was.
return true;
}
if ($enrolstart < $starttime && $endtime < $enrolend) {
/* The enrolment is active during all the analysis time.
* |-----------------------------|
* |========|
* E start A start A end E end
*/
return true;
}
// If we reach this point is because the enrolment is only active for a portion of the analysis interval.
// Therefore, we check that it was active for most of the analysis interval, a self::ENROL_ACTIVE_PERCENT_REQUIRED.
if ($starttime <= $enrolstart && $enrolend <= $endtime) {
/* |=============================|
* |--------|
* A start E start E end A end
*/
$activeenrolduration = $enrolend - $enrolstart;
} else if ($enrolstart <= $starttime && $enrolend <= $endtime) {
/* |===================|
* |------------------|
* E start A start E end A end
*/
$activeenrolduration = $enrolend - $starttime;
} else if ($starttime <= $enrolstart && $endtime <= $enrolend) {
/* |===================|
* |------------------|
* A start E start A end E end
*/
$activeenrolduration = $endtime - $enrolstart;
}
$analysisduration = $endtime - $starttime;
if (floatval($activeenrolduration) / floatval($analysisduration) < self::ENROL_ACTIVE_PERCENT_REQUIRED) {
// The student was not enroled in the course for most of the analysis interval.
return false;
}
// We happily return true if the enrolment was active for more than self::ENROL_ACTIVE_PERCENT_REQUIRED of
// the analysis interval.
return true;
}
}
@@ -0,0 +1,186 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Getting the minimum grade to pass target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Getting the minimum grade to pass target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_gradetopass extends course_enrolments {
/**
* Courses grades to pass.
* @var mixed[]
*/
protected $coursesgradetopass = array();
/**
* Courses grades.
* @var mixed[]
*/
protected $coursesgrades = array();
/**
* Returns the grade to pass a course.
*
* Save the value in $coursesgradetopass array to prevent new accesses to the database.
*
* @param int $courseid The course id.
* @return array The courseitem id and the required grade to pass the course.
*/
protected function get_course_gradetopass($courseid) {
if (!isset($this->coursesgradetopass[$courseid])) {
// Get course grade_item.
$courseitem = \grade_item::fetch_course_item($courseid);
$ci = array();
$ci['courseitemid'] = $courseitem->id;
if ($courseitem->gradetype == GRADE_TYPE_VALUE && grade_floats_different($courseitem->gradepass, 0.0)) {
$ci['gradetopass'] = $courseitem->gradepass;
} else {
$ci['gradetopass'] = null;
}
$this->coursesgradetopass[$courseid] = $ci;
}
return $this->coursesgradetopass[$courseid];
}
/**
* Returns the grade of a user in a course.
*
* Saves the grades of all course users in $coursesgrades array to prevent new accesses to the database.
*
* @param int $courseitemid The course item id.
* @param int $userid the user whose grade is requested.
* @return array The courseitem id and the required grade to pass the course.
*/
protected function get_user_grade($courseitemid, $userid) {
// If the user grade for this course is not available, get all the grades for the course.
if (!isset($this->coursesgrades[$courseitemid])) {
// Ony a course is cached to avoid high memory usage.
unset($this->coursesgrades);
$gg = new \grade_grade(null, false);
$usersgrades = $gg->fetch_all(array('itemid' => $courseitemid));
if ($usersgrades) {
foreach ($usersgrades as $ug) {
$this->coursesgrades[$courseitemid][$ug->userid] = $ug->finalgrade;
}
}
}
if (!isset($this->coursesgrades[$courseitemid][$userid])) {
$this->coursesgrades[$courseitemid][$userid] = null;
}
return $this->coursesgrades[$courseitemid][$userid];
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursegradetopass', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentgradetopassno', 'course'),
get_string('targetlabelstudentgradetopassyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* Only courses with "value" grade type and grade to pass set are valid.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
$courseitem = $this->get_course_gradetopass ($course->get_id());
if (is_null($courseitem['gradetopass'])) {
return get_string('gradetopassnotset', 'course');
}
return true;
}
/**
* The user's grade in the course sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> course grade to pass achieved, 1 -> course grade to pass not achieved
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// Get course grade to pass.
$courseitem = $this->get_course_gradetopass($course->get_id());
// Get the user grade.
$usergrade = $this->get_user_grade($courseitem['courseitemid'], $userenrol->userid);
if ($usergrade >= $courseitem['gradetopass']) {
return 0;
}
return 1;
}
}
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* No accesses since the start of the course.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No accesses since the start of the course.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_access_since_course_start extends no_recent_accesses {
/**
* Only past stuff whose start matches the course start.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\after_start);
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:noaccesssincecoursestart', 'course');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return array The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('noaccesssincestartinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('noaccesssincestartinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
}
@@ -0,0 +1,144 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* No recent accesses.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No recent accesses.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_recent_accesses extends course_enrolments {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:norecentaccesses', 'course');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return array The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('norecentaccessesinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('norecentaccessesinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* Only past stuff whose start matches the course start.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\past_periodic);
}
/**
* Discards courses that are not yet ready to be used for prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
if (!$course->was_started()) {
return get_string('coursenotyetstarted', 'course');
}
if (!$this->students = $course->get_students()) {
return get_string('nocoursestudents', 'course');
}
if (!$fortraining && !$course->get_course_data()->visible) {
return get_string('hiddenfromstudents');
}
if ($course->get_end() && $course->get_end() < $course->get_start()) {
return get_string('errorendbeforestart', 'course');
}
// Finished courses can not be used to get predictions.
if (!$fortraining && $course->is_finished()) {
return get_string('coursealreadyfinished', 'course');
}
return true;
}
/**
* Do the user has any read action in the course?
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> accesses, 1 -> no accesses.
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$readactions = $this->retrieve('\core\analytics\indicator\any_course_access', $sampleid);
if ($readactions == \core\analytics\indicator\any_course_access::get_min_value()) {
return 1;
}
return 0;
}
}
@@ -0,0 +1,212 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* No teaching target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No teaching target.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_teaching extends \core_analytics\local\target\binary {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* It requires a specific time-splitting method.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return (get_class($timesplitting) === \core\analytics\time_splitting\single_range::class);
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:noteachingactivity', 'course');
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
public function get_insight_subject(int $modelid, \context $context) {
return get_string('noteachingupcomingcourses');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return string[] The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['userfirstname' => $user->firstname];
$fullmessage = get_string('noteachinginfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('noteachinginfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param mixed $includedetailsaction
* @param bool $isinsightuser
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
$isinsightuser = false) {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
$sampledata = $prediction->get_sample_data();
$course = $sampledata['course'];
$actions = array();
$url = new \moodle_url('/course/view.php', array('id' => $course->id));
$pix = new \pix_icon('i/course', get_string('course'));
$actions[] = new \core_analytics\prediction_action('viewcourse', $prediction,
$url, $pix, get_string('view'));
if (course_can_view_participants($sampledata['context'])) {
$url = new \moodle_url('/user/index.php', array('id' => $course->id));
$pix = new \pix_icon('i/cohort', get_string('participants'));
$actions[] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$url, $pix, get_string('participants'));
}
$parentactions = parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser);
return array_merge($actions, $parentactions);
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelteachingyes', 'course'),
get_string('targetlabelteachingno', 'course'),
);
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\site_courses';
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param mixed $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// The analysable is the site, so yes, it is always valid.
return true;
}
/**
* Only process samples which start date is getting close.
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
$course = $this->retrieve('course', $sampleid);
$now = time();
// No courses without start date, no finished courses, no predictions before start - 1 week nor
// predictions for courses that started more than 1 week ago.
if (!$course->startdate || (!empty($course->enddate) && $course->enddate < $now) ||
$course->startdate - WEEKSECS > $now || $course->startdate + WEEKSECS < $now) {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$noteachersindicator = $this->retrieve('\core_course\analytics\indicator\no_teacher', $sampleid);
$nostudentsindicator = $this->retrieve('\core_course\analytics\indicator\no_student', $sampleid);
if ($noteachersindicator == \core_course\analytics\indicator\no_teacher::get_min_value() ||
$nostudentsindicator == \core_course\analytics\indicator\no_student::get_min_value()) {
// No teachers or no students :(.
return 1;
}
return 0;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\cache;
use cache_data_source;
use cache_definition;
use moodle_url;
use core_course_list_element;
/**
* Class to describe cache data source for course image.
*
* @package core
* @subpackage course
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_image implements cache_data_source {
/** @var course_image */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return \core_course\cache\course_image
*/
public static function get_instance_for_cache(cache_definition $definition): course_image {
if (is_null(self::$instance)) {
self::$instance = new course_image();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return string|bool Returns course image url as a string or false if the image is not exist
*/
public function load_for_cache($key) {
// We should use get_course() instead of get_fast_modinfo() for better performance.
$course = get_course($key);
return $this->get_image_url_from_overview_files($course);
}
/**
* Returns image URL from course overview files.
*
* @param \stdClass $course Course object.
* @return null|string Image URL or null if it's not exists.
*/
protected function get_image_url_from_overview_files(\stdClass $course): ?string {
$courseinlist = new core_course_list_element($course);
foreach ($courseinlist->get_course_overviewfiles() as $file) {
if ($file->is_valid_image()) {
return moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
null,
$file->get_filepath(),
$file->get_filename()
)->out();
}
}
// Returning null if no image found to let it be cached
// as false is what cache API returns then a data is not found in cache.
return null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys): array {
$records = [];
foreach ($keys as $key) {
$records[$key] = $this->load_for_cache($key);
}
return $records;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course handler for custom fields
*
* @package core_course
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\customfield;
defined('MOODLE_INTERNAL') || die;
use core_customfield\api;
use core_customfield\field_controller;
/**
* Course handler for custom fields
*
* @package core_course
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_handler extends \core_customfield\handler {
/**
* @var course_handler
*/
static protected $singleton;
/**
* @var \context
*/
protected $parentcontext;
/** @var int Field is displayed in the course listing, visible to everybody */
const VISIBLETOALL = 2;
/** @var int Field is displayed in the course listing but only for teachers */
const VISIBLETOTEACHERS = 1;
/** @var int Field is not displayed in the course listing */
const NOTVISIBLE = 0;
/**
* Returns a singleton
*
* @param int $itemid
* @return \core_course\customfield\course_handler
*/
public static function create(int $itemid = 0): \core_customfield\handler {
if (static::$singleton === null) {
self::$singleton = new static(0);
}
return self::$singleton;
}
/**
* Run reset code after unit tests to reset the singleton usage.
*/
public static function reset_caches(): void {
if (!PHPUNIT_TEST) {
throw new \coding_exception('This feature is only intended for use in unit tests');
}
static::$singleton = null;
}
/**
* The current user can configure custom fields on this component.
*
* @return bool true if the current can configure custom fields, false otherwise
*/
public function can_configure(): bool {
return has_capability('moodle/course:configurecustomfields', $this->get_configuration_context());
}
/**
* The current user can edit custom fields on the given course.
*
* @param field_controller $field
* @param int $instanceid id of the course to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_edit(field_controller $field, int $instanceid = 0): bool {
if ($instanceid) {
$context = $this->get_instance_context($instanceid);
return (!$field->get_configdata_property('locked') ||
has_capability('moodle/course:changelockedcustomfields', $context));
} else {
$context = $this->get_parent_context();
if ($context->contextlevel == CONTEXT_SYSTEM) {
return (!$field->get_configdata_property('locked') ||
has_capability('moodle/course:changelockedcustomfields', $context));
} else {
return (!$field->get_configdata_property('locked') ||
guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context));
}
}
}
/**
* The current user can view custom fields on the given course.
*
* @param field_controller $field
* @param int $instanceid id of the course to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_view(field_controller $field, int $instanceid): bool {
$visibility = $field->get_configdata_property('visibility');
if ($visibility == self::NOTVISIBLE) {
return false;
} else if ($visibility == self::VISIBLETOTEACHERS) {
return has_capability('moodle/course:update', $this->get_instance_context($instanceid));
} else {
return true;
}
}
/**
* Sets parent context for the course
*
* This may be needed when course is being created, there is no course context but we need to check capabilities
*
* @param \context $context
*/
public function set_parent_context(\context $context) {
$this->parentcontext = $context;
}
/**
* Returns the parent context for the course
*
* @return \context
*/
protected function get_parent_context(): \context {
global $PAGE;
if ($this->parentcontext) {
return $this->parentcontext;
} else if ($PAGE->context && $PAGE->context instanceof \context_coursecat) {
return $PAGE->context;
}
return \context_system::instance();
}
/**
* Context that should be used for new categories created by this handler
*
* @return \context the context for configuration
*/
public function get_configuration_context(): \context {
return \context_system::instance();
}
/**
* URL for configuration of the fields on this handler.
*
* @return \moodle_url The URL to configure custom fields for this component
*/
public function get_configuration_url(): \moodle_url {
return new \moodle_url('/course/customfield.php');
}
/**
* Returns the context for the data associated with the given instanceid.
*
* @param int $instanceid id of the record to get the context for
* @return \context the context for the given record
*/
public function get_instance_context(int $instanceid = 0): \context {
if ($instanceid > 0) {
return \context_course::instance($instanceid);
} else {
return \context_system::instance();
}
}
/**
* Allows to add custom controls to the field configuration form that will be saved in configdata
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'course_handler_header', get_string('customfieldsettings', 'core_course'));
$mform->setExpanded('course_handler_header', true);
// If field is locked.
$mform->addElement('selectyesno', 'configdata[locked]', get_string('customfield_islocked', 'core_course'));
$mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'core_course');
// Field data visibility.
$visibilityoptions = [self::VISIBLETOALL => get_string('customfield_visibletoall', 'core_course'),
self::VISIBLETOTEACHERS => get_string('customfield_visibletoteachers', 'core_course'),
self::NOTVISIBLE => get_string('customfield_notvisible', 'core_course')];
$mform->addElement('select', 'configdata[visibility]', get_string('customfield_visibility', 'core_course'),
$visibilityoptions);
$mform->addHelpButton('configdata[visibility]', 'customfield_visibility', 'core_course');
}
/**
* Creates or updates custom field data.
*
* @param \restore_task $task
* @param array $data
*
* @return int|void Conditionally returns the ID of the created or updated record.
*/
public function restore_instance_data_from_backup(\restore_task $task, array $data) {
$courseid = $task->get_courseid();
$context = $this->get_instance_context($courseid);
$editablefields = $this->get_editable_fields($courseid);
$records = api::get_instance_fields_data($editablefields, $courseid);
$target = $task->get_target();
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
foreach ($records as $d) {
$field = $d->get_field();
if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) {
if (!$d->get('id') || $override) {
$d->set($d->datafield(), $data['value']);
$d->set('value', $data['value']);
$d->set('valueformat', $data['valueformat']);
$d->set('valuetrust', !empty($data['valuetrust']));
$d->set('contextid', $context->id);
$d->save();
}
return $d->get('id');
}
}
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Delete category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir . '/formslib.php');
require_once($CFG->libdir . '/questionlib.php');
/**
* Delete category moodleform.
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_deletecategory_form extends moodleform {
/**
* The core_course_category object for that category being deleted.
* @var core_course_category
*/
protected $coursecat;
/**
* Defines the form.
*/
public function definition() {
$mform = $this->_form;
$this->coursecat = $this->_customdata;
$categorycontext = context_coursecat::instance($this->coursecat->id);
$categoryname = $this->coursecat->get_formatted_name();
// Check permissions, to see if it OK to give the option to delete
// the contents, rather than move elsewhere.
$candeletecontent = $this->coursecat->can_delete_full();
// Get the list of categories we might be able to move to.
$displaylist = $this->coursecat->move_content_targets_list();
// Now build the options.
$options = array();
if ($displaylist) {
$options[0] = get_string('movecontentstoanothercategory');
}
if ($candeletecontent) {
$options[1] = get_string('deleteallcannotundo');
}
if (empty($options)) {
throw new \moodle_exception('youcannotdeletecategory', 'error', 'index.php', $categoryname);
}
// Now build the form.
$mform->addElement('header', 'general', get_string('categorycurrentcontents', '', $categoryname));
// Describe the contents of this category.
$contents = '';
if ($this->coursecat->has_children()) {
$contents .= html_writer::tag('li', get_string('subcategories'));
}
if ($this->coursecat->has_courses()) {
$contents .= html_writer::tag('li', get_string('courses'));
}
if (question_context_has_any_questions($categorycontext)) {
$contents .= html_writer::tag('li', get_string('questionsinthequestionbank'));
}
// Check if plugins can provide more info.
$pluginfunctions = $this->coursecat->get_plugins_callback_function('get_course_category_contents');
foreach ($pluginfunctions as $pluginfunction) {
if ($plugincontents = $pluginfunction($this->coursecat)) {
$contents .= html_writer::tag('li', $plugincontents);
}
}
if (!empty($contents)) {
$mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
} else {
$mform->addElement('static', 'emptymessage', '', get_string('deletecategoryempty'));
}
// Give the options for what to do.
$mform->addElement('select', 'fulldelete', get_string('whattodo'), $options);
if (count($options) == 1) {
// Freeze selector if only one option available.
$optionkeys = array_keys($options);
$option = reset($optionkeys);
$mform->hardFreeze('fulldelete');
$mform->setConstant('fulldelete', $option);
}
if ($displaylist) {
$mform->addElement('autocomplete', 'newparent', get_string('movecategorycontentto'), $displaylist);
if (in_array($this->coursecat->parent, $displaylist)) {
$mform->setDefault('newparent', $this->coursecat->parent);
}
$mform->hideIf('newparent', 'fulldelete', 'eq', '1');
}
$mform->addElement('hidden', 'categoryid', $this->coursecat->id);
$mform->setType('categoryid', PARAM_ALPHANUM);
$mform->addElement('hidden', 'action', 'deletecategory');
$mform->setType('action', PARAM_ALPHANUM);
$this->add_action_buttons(true, get_string('delete'));
}
/**
* Perform some extra moodle validation.
*
* @param array $data
* @param array $files
* @return array An array of errors.
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (empty($data['fulldelete']) && empty($data['newparent'])) {
// When they have chosen the move option, they must specify a destination.
$errors['newparent'] = get_string('required');
return $errors;
}
if (!empty($data['newparent']) && !$this->coursecat->can_move_content_to($data['newparent'])) {
$errors['newparent'] = get_string('movecatcontentstoselected', 'error');
}
return $errors;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Edit category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir.'/formslib.php');
/**
* Edit category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_editcategory_form extends moodleform {
/**
* The form definition.
*/
public function definition() {
global $CFG, $DB;
$mform = $this->_form;
$categoryid = $this->_customdata['categoryid'];
$parent = $this->_customdata['parent'];
// Get list of categories to use as parents, with site as the first one.
$options = array();
if (has_capability('moodle/category:manage', context_system::instance()) || $parent == 0) {
$options[0] = get_string('top');
}
if ($categoryid) {
// Editing an existing category.
$options += core_course_category::make_categories_list('moodle/category:manage', $categoryid);
if (empty($options[$parent])) {
// Ensure the the category parent has been included in the options.
$options[$parent] = $DB->get_field('course_categories', 'name', array('id'=>$parent));
}
$strsubmit = get_string('savechanges');
} else {
// Making a new category.
$options += core_course_category::make_categories_list('moodle/category:manage');
$strsubmit = get_string('createcategory');
}
$mform->addElement('autocomplete', 'parent', get_string('parentcategory'), $options);
$mform->addRule('parent', null, 'required', null, 'client');
$mform->addElement('text', 'name', get_string('categoryname'), array('size' => '30'));
$mform->addRule('name', get_string('required'), 'required', null);
$mform->setType('name', PARAM_TEXT);
$mform->addElement('text', 'idnumber', get_string('idnumbercoursecategory'), 'maxlength="100" size="10"');
$mform->addHelpButton('idnumber', 'idnumbercoursecategory');
$mform->setType('idnumber', PARAM_RAW);
$mform->addElement('editor', 'description_editor', get_string('description'), null,
$this->get_description_editor_options());
$mform->setType('description_editor', PARAM_RAW);
if (!empty($CFG->allowcategorythemes)) {
$themes = array(''=>get_string('forceno'));
$allthemes = get_list_of_themes();
foreach ($allthemes as $key => $theme) {
if (empty($theme->hidefromselector)) {
$themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
}
}
$mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
}
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
$mform->setDefault('id', $categoryid);
$this->add_action_buttons(true, $strsubmit);
}
/**
* Returns the description editor options.
* @return array
*/
public function get_description_editor_options() {
global $CFG;
$context = $this->_customdata['context'];
$itemid = $this->_customdata['itemid'];
return array(
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
'trusttext' => false,
'noclean' => true,
'context' => $context,
'subdirs' => file_area_contains_subdirs($context, 'coursecat', 'description', $itemid),
);
}
/**
* Validates the data submit for this form.
*
* @param array $data An array of key,value data pairs.
* @param array $files Any files that may have been submit as well.
* @return array An array of errors.
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (!empty($data['idnumber'])) {
if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) {
if (!$data['id'] || $existing->id != $data['id']) {
$errors['idnumber'] = get_string('categoryidnumbertaken', 'error');
}
}
}
return $errors;
}
}
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class for exporting a course module summary from an stdClass.
*
* @package core_course
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
/**
* Class for exporting a course module summary from a cm_info class.
*
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_summary_exporter extends \core\external\exporter {
protected static function define_related() {
return array('cm' => 'cm_info');
}
protected function get_other_values(renderer_base $output) {
$cm = $this->related['cm'];
$values = array(
'id' => $cm->id,
'name' => $cm->name,
'iconurl' => $cm->get_icon_url()->out()
);
if ($cm->url) {
$values['url'] = $cm->url->out();
}
return $values;
}
/**
* Get the format parameters for name.
*
* @return array
*/
protected function get_format_parameters_for_name() {
$cm = $this->related['cm'];
$context = $cm->context;
return [
'context' => $context,
];
}
public static function define_other_properties() {
return array(
'id' => array(
'type' => PARAM_INT,
),
'name' => array(
'type' => PARAM_TEXT
),
'url' => array(
'type' => PARAM_URL,
'optional' => true,
),
'iconurl' => array(
'type' => PARAM_URL
)
);
}
}
+233
View File
@@ -0,0 +1,233 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class for exporting a course summary from an stdClass.
*
* @package core_course
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
use moodle_url;
/**
* Class for exporting a course summary from an stdClass.
*
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_summary_exporter extends \core\external\exporter {
/**
* Constructor - saves the persistent object, and the related objects.
*
* @param mixed $data - Either an stdClass or an array of values.
* @param array $related - An optional list of pre-loaded objects related to this object.
*/
public function __construct($data, $related = array()) {
if (!array_key_exists('isfavourite', $related)) {
$related['isfavourite'] = false;
}
parent::__construct($data, $related);
}
protected static function define_related() {
// We cache the context so it does not need to be retrieved from the course.
return array('context' => '\\context', 'isfavourite' => 'bool?');
}
protected function get_other_values(renderer_base $output) {
global $CFG;
$courseimage = self::get_course_image($this->data);
if (!$courseimage) {
$courseimage = $output->get_generated_image_for_id($this->data->id);
}
$progress = self::get_course_progress($this->data);
$hasprogress = false;
if ($progress === 0 || $progress > 0) {
$hasprogress = true;
}
$progress = floor($progress ?? 0);
$coursecategory = \core_course_category::get($this->data->category, MUST_EXIST, true);
return array(
'fullnamedisplay' => get_course_display_name_for_list($this->data),
'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false),
'courseimage' => $courseimage,
'progress' => $progress,
'hasprogress' => $hasprogress,
'isfavourite' => $this->related['isfavourite'],
'hidden' => boolval(get_user_preferences('block_myoverview_hidden_course_' . $this->data->id, 0)),
'showshortname' => $CFG->courselistshortnames ? true : false,
'coursecategory' => $coursecategory->name
);
}
public static function define_properties() {
return array(
'id' => array(
'type' => PARAM_INT,
),
'fullname' => array(
'type' => PARAM_TEXT,
),
'shortname' => array(
'type' => PARAM_TEXT,
),
'idnumber' => array(
'type' => PARAM_RAW,
),
'summary' => array(
'type' => PARAM_RAW,
'null' => NULL_ALLOWED,
'default' => null,
),
'summaryformat' => array(
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
),
'startdate' => array(
'type' => PARAM_INT,
),
'enddate' => array(
'type' => PARAM_INT,
),
'visible' => array(
'type' => PARAM_BOOL,
),
'showactivitydates' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED
],
'showcompletionconditions' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED
],
'pdfexportfont' => [
'type' => PARAM_TEXT,
'null' => NULL_ALLOWED,
'default' => null,
],
);
}
/**
* Get the formatting parameters for the summary.
*
* @return array
*/
protected function get_format_parameters_for_summary() {
return [
'component' => 'course',
'filearea' => 'summary',
];
}
public static function define_other_properties() {
return array(
'fullnamedisplay' => array(
'type' => PARAM_TEXT,
),
'viewurl' => array(
'type' => PARAM_URL,
),
'courseimage' => array(
'type' => PARAM_RAW,
),
'progress' => array(
'type' => PARAM_INT,
'optional' => true
),
'hasprogress' => array(
'type' => PARAM_BOOL
),
'isfavourite' => array(
'type' => PARAM_BOOL
),
'hidden' => array(
'type' => PARAM_BOOL
),
'timeaccess' => array(
'type' => PARAM_INT,
'optional' => true
),
'showshortname' => array(
'type' => PARAM_BOOL
),
'coursecategory' => array(
'type' => PARAM_TEXT
)
);
}
/**
* Get the course image if added to course.
*
* @param object $course
* @return string|false url of course image or false if it's not exist.
*/
public static function get_course_image($course) {
$image = \cache::make('core', 'course_image')->get($course->id);
if (is_null($image)) {
$image = false;
}
return $image;
}
/**
* Get the course pattern datauri.
*
* The datauri is an encoded svg that can be passed as a url.
* @param object $course
* @return string datauri
* @deprecated 3.7
*/
public static function get_course_pattern($course) {
global $OUTPUT;
debugging('course_summary_exporter::get_course_pattern() is deprecated. ' .
'Please use $OUTPUT->get_generated_image_for_id() instead.', DEBUG_DEVELOPER);
return $OUTPUT->get_generated_image_for_id($course->id);
}
/**
* Get the course progress percentage.
*
* @param object $course
* @return int progress
*/
public static function get_course_progress($course) {
return \core_completion\progress::get_course_progress_percentage($course);
}
/**
* Get the course color.
*
* @param int $courseid
* @return string hex color code.
* @deprecated 3.7
*/
public static function coursecolor($courseid) {
global $OUTPUT;
debugging('course_summary_exporter::coursecolor() is deprecated. ' .
'Please use $OUTPUT->get_generated_color_for_id() instead.', DEBUG_DEVELOPER);
return $OUTPUT->get_generated_color_for_id($courseid);
}
}
@@ -0,0 +1,220 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\external;
defined('MOODLE_INTERNAL') || die();
use context_user;
use core_calendar_external;
use core_course_external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
require_once("{$CFG->dirroot}/calendar/externallib.php");
require_once("{$CFG->dirroot}/course/externallib.php");
/**
* Class for fetching courses which have action event(s) and match given filter parameters.
*
* @package core_course
* @copyright 2022 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_enrolled_courses_with_action_events_by_timeline_classification extends external_api {
/**
* Returns the description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'),
'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0),
'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0),
'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null),
'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield',
VALUE_DEFAULT, null),
'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield',
VALUE_DEFAULT, null),
'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against',
VALUE_DEFAULT, null),
'eventsfrom' => new external_value(PARAM_INT, 'Optional starting timestamp for action events',
VALUE_DEFAULT, null),
'eventsto' => new external_value(PARAM_INT, 'Optional ending timestamp for action events',
VALUE_DEFAULT, null),
]
);
}
/**
* Get courses matching the given timeline classification which have action event(s).
*
* Fetches courses by timeline classification which have at least one action event within the specified filtering.
*
* @param string $classification past, inprogress, or future
* @param int $limit Number of courses with events to attempt to fetch
* @param int $offset Offset the full course set before timeline classification is applied
* @param string $sort SQL sort string for results
* @param string $customfieldname Custom field name used when when classification is customfield
* @param string $customfieldvalue Custom field value used when when classification is customfield
* @param string $searchvalue Text search being applied
* @param int $eventsfrom The start timestamp (inclusive) to search from for action events in the course
* @param int $eventsto The end timestamp (inclusive) to search to for action events in the course
* @return array list of courses and any warnings
*/
public static function execute(
string $classification,
int $limit = 0,
int $offset = 0,
string $sort = null,
string $customfieldname = null,
string $customfieldvalue = null,
string $searchvalue = null,
int $eventsfrom = null,
int $eventsto = null
): array {
global $USER;
self::validate_context(context_user::instance($USER->id));
$params = self::validate_parameters(
self::execute_parameters(),
[
'classification' => $classification,
'limit' => $limit,
'offset' => $offset,
'sort' => $sort,
'customfieldname' => $customfieldname,
'customfieldvalue' => $customfieldvalue,
'searchvalue' => $searchvalue,
'eventsfrom' => $eventsfrom,
'eventsto' => $eventsto,
]
);
$classification = $params['classification'];
$limit = $params['limit'];
$offset = $params['offset'];
$sort = $params['sort'];
$customfieldname = $params['customfieldname'];
$customfieldvalue = $params['customfieldvalue'];
$searchvalue = clean_param($params['searchvalue'], PARAM_TEXT);
$eventsfrom = $params['eventsfrom'];
$eventsto = $params['eventsto'];
$morecoursestofetch = true;
$morecoursespossible = true;
$coursesfinal = [];
do {
// Fetch courses.
[
'courses' => $coursesfetched,
'nextoffset' => $offset,
] = core_course_external::get_enrolled_courses_by_timeline_classification($classification, $limit,
$offset, $sort, $customfieldname, $customfieldvalue, $searchvalue);
$courseids = array_column($coursesfetched, 'id');
$coursesfetched = array_combine($courseids, $coursesfetched);
if (!empty($courseids)) {
// Need to check this to know how many are expected (since it is possible for this to be less than the limit).
$numcoursesfetched = count($courseids);
$numfetchedwithevents = 0;
// If less courses are fetched than we requested, we know it is not possible for more courses to be available.
if ($numcoursesfetched < $limit) {
$morecoursestofetch = false;
$morecoursespossible = false;
}
// Try to fetch one action event within the time/search parameters for each course.
$events = core_calendar_external::get_calendar_action_events_by_courses($courseids, $eventsfrom, $eventsto, 1,
$searchvalue);
foreach ($events->groupedbycourse as $courseevents) {
$courseid = $courseevents->courseid;
// Only include courses which contain at least one event.
if (empty($courseevents->events)) {
unset($coursesfetched[$courseid]);
} else {
$numfetchedwithevents++;
}
}
// Add courses with events to the final course list in order.
$coursesfinal = array_merge($coursesfinal, $coursesfetched);
// If any courses did not have events, adjust the limit so we can attempt to fetch as many as are still required.
if ($numfetchedwithevents < $numcoursesfetched) {
$limit -= $numfetchedwithevents;
} else {
// If we have found as many courses as required or are available, no need to attempt fetching more.
$morecoursestofetch = false;
}
} else {
$morecoursestofetch = false;
$morecoursespossible = false;
}
} while ($morecoursestofetch);
static $isrecursivecall = false;
$morecoursesavailable = false;
// Recursively call this method to check if at least one more course is available if we know that is a possibility.
if (!$isrecursivecall && $morecoursespossible) {
// Prevent infinite recursion.
$isrecursivecall = true;
$additionalcourses = self::execute(
$classification, 1, $offset, $sort, $customfieldname, $customfieldvalue, $searchvalue, $eventsfrom, $eventsto
);
if (!empty($additionalcourses['courses'])) {
$morecoursesavailable = true;
}
}
return [
'courses' => $coursesfinal,
'nextoffset' => $offset,
'morecoursesavailable' => $morecoursesavailable,
];
}
/**
* Returns description of method result value.
*
* @return \core_external\external_description
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'courses' => new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Course'),
'nextoffset' => new external_value(PARAM_INT, 'Offset for the next request'),
'morecoursesavailable' => new external_value(PARAM_BOOL,
'Whether more courses with events exist within the provided parameters'),
]
);
}
}
@@ -0,0 +1,137 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\external;
use context_module;
use core_external\external_description;
use core_external\external_files;
use core_external\external_format_value;
use core_external\util as external_util;
use core_external\external_value;
/**
* This class helps implement the get_..._by_courses web service that every activity should have.
*
* It has helper methods to add the standard course-module fields to the results, and the declaration of the return value.
*
* @package core_course
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class helper_for_get_mods_by_courses {
/**
* Add the value of all the standard fields to the results to be returned by the service.
* This is designed to be used in the implementation of the get_..._by_courses web service methods.
*
* Note that $modinstance is also updated in-place.
*
* @param \stdClass $modinstance one of the objects returned from a call to {@see get_all_instances_in_courses()}.
* @param string $component the plugin name, e.g. 'mod_book'.
* @param string $capabilityforgroups capability to check before including group/visible/section info in the results.
* @param string|null $capabilityforintro capability to check before including intro info in the results.
* null means always include (the default).
* @return array with the containing all the values declared in {@see standard_coursemodule_elements_returns()}.
*/
public static function standard_coursemodule_element_values(\stdClass $modinstance, string $component,
string $capabilityforgroups = 'moodle/course:manageactivities', string $capabilityforintro = null): array {
self::format_name_and_intro($modinstance, $component);
$context = context_module::instance($modinstance->coursemodule);
// First, we return information that any user can see in the web interface.
$moddetails['id'] = $modinstance->id;
$moddetails['coursemodule'] = $modinstance->coursemodule;
$moddetails['course'] = $modinstance->course;
$moddetails['name'] = $modinstance->name;
$moddetails['lang'] = clean_param($modinstance->lang, PARAM_LANG);
if (!$capabilityforintro || has_capability($capabilityforintro, $context)) {
$moddetails['intro'] = $modinstance->intro;
$moddetails['introformat'] = $modinstance->introformat;
$moddetails['introfiles'] = $modinstance->introfiles;
}
// Now add information only available to people who can edit.
if (has_capability($capabilityforgroups, $context)) {
$moddetails['section'] = $modinstance->section;
$moddetails['visible'] = $modinstance->visible;
$moddetails['groupmode'] = $modinstance->groupmode;
$moddetails['groupingid'] = $modinstance->groupingid;
}
return $moddetails;
}
/**
* Format the module name an introduction ready to be exported to a web service.
*
* Note that $modinstance is updated in-place.
*
* @param \stdClass $modinstance one of the objects returned from a call to {@see get_all_instances_in_courses()}.
* @param string $component the plugin name, e.g. 'mod_book'.
*/
public static function format_name_and_intro(\stdClass $modinstance, string $component) {
$context = context_module::instance($modinstance->coursemodule);
$modinstance->name = \core_external\util::format_string($modinstance->name, $context);
[$modinstance->intro, $modinstance->introformat] = \core_external\util::format_text(
$modinstance->intro, $modinstance->introformat, $context,
$component, 'intro', null, ['noclean' => true]);
$modinstance->introfiles = external_util::get_area_files($context->id, $component, 'intro', false, false);
}
/**
* Get the list of standard fields, to add to the declaration of the return values.
*
* Example usage combine the fields returned here with any extra ones your activity uses:
*
* public static function execute_returns() {
* return new external_single_structure([
* 'bigbluebuttonbns' => new external_multiple_structure(
* new external_single_structure(array_merge(
* helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(),
* [
* 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'),
* 'timemodified' => new external_value(PARAM_INT, 'Last time the instance was modified'),
* ]
* ))
* ),
* 'warnings' => new external_warnings(),
* ]
* );
* }
*
* @param bool $introoptional if true, the intro fields are marked as optional. Default false.
* @return external_description[] array of standard fields, to which you can add your activity-specific ones.
*/
public static function standard_coursemodule_elements_returns(bool $introoptional = false): array {
return [
'id' => new external_value(PARAM_INT, 'Activity instance id'),
'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
'course' => new external_value(PARAM_INT, 'Course id'),
'name' => new external_value(PARAM_RAW, 'Activity name'),
'intro' => new external_value(PARAM_RAW, 'Activity introduction', $introoptional ? VALUE_OPTIONAL : VALUE_REQUIRED),
'introformat' => new external_format_value('intro', $introoptional ? VALUE_OPTIONAL : VALUE_REQUIRED),
'introfiles' => new external_files('Files in the introduction', VALUE_OPTIONAL),
'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL),
];
}
}
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use stdClass;
/**
* Hook after course creation.
*
* This hook will be dispatched after the course is created and events are fired.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions after a course is created.')]
#[\core\attribute\tags('course')]
class after_course_created {
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
) {
}
}
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use stdClass;
/**
* Hook after course updates.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions after a course is updated.')]
#[\core\attribute\tags('course')]
class after_course_updated {
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
* @param stdClass $oldcourse The old course instance.
* @param bool $changeincoursecat Whether the course category has changed.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
/** @var stdClass The old course instance */
public readonly stdClass $oldcourse,
/** @var bool Whether the course category has changed */
public readonly bool $changeincoursecat = false,
) {
}
}
@@ -0,0 +1,48 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use core\hook\described_hook;
use course_edit_form;
use MoodleQuickForm;
/**
* Allows plugins to extend course form definition and add/remove/update form elements.
*
* @see course_edit_form::definition()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend course editing form')]
#[\core\attribute\tags('course')]
class after_form_definition {
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper.
* @param MoodleQuickForm $mform Form to be extended.
*/
public function __construct(
/** @var course_edit_form The form wrapper for the edit form */
public readonly course_edit_form $formwrapper,
/** @var MoodlequickForm The form to be extended */
public readonly MoodleQuickForm $mform,
) {
}
}
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use course_edit_form;
use MoodleQuickForm;
/**
* Allows plugins to extend course form after data is set.
*
* @see course_edit_form::definition_after_data()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend course editing form after data is set')]
#[\core\attribute\tags('course')]
class after_form_definition_after_data {
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper..
* @param MoodleQuickForm $mform Form to be extended.
*/
public function __construct(
/** @var course_edit_form The form wrapper for the edit form */
public readonly course_edit_form $formwrapper,
/** @var MoodlequickForm The form to be extended */
public readonly MoodleQuickForm $mform,
) {
}
}
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
/**
* Allows plugins to extend course form submission.
*
* @see create_course()
* @see update_course()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend saving of the course editing form')]
#[\core\attribute\tags('course')]
class after_form_submission {
/**
* Creates new hook.
*
* @param \stdClass $data Submitted data
* @param bool $isnewcourse Whether this is a new course
*/
public function __construct(
/** @var \stdClass The submitted data */
protected \stdClass $data,
/** @var bool Whether this is a new course */
public readonly bool $isnewcourse = false,
) {
}
/**
* Returns submitted data.
*
* @return \stdClass
*/
public function get_data(): \stdClass {
return $this->data;
}
}
@@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use course_edit_form;
/**
* Allows plugins to extend course form validation.
*
* @see course_edit_form::validation()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allow plugins to extend a validation of the course editing form')]
#[\core\attribute\tags('course')]
class after_form_validation {
/**
* Plugin errors.
*
* @var array
*/
protected $errors = [];
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper..
* @param array $data Submitted data.
* @param array $files Submitted files.
*/
public function __construct(
/** @var course_edit_form Course form wrapper */
public readonly course_edit_form $formwrapper,
/** @var array The submitted data */
private array $data,
/** @var array Submitted files */
private array $files = [],
) {
}
/**
* Returns submitted data.
*
* @return array
*/
public function get_data(): array {
return $this->data;
}
/**
* Returns submitted files.
*
* @return array
*/
public function get_files(): array {
return $this->files;
}
/**
* Return plugin generated errors.
*
* @return array
*/
public function get_errors(): array {
return $this->errors;
}
/**
* Plugins implementing a callback can add validation errors.
*
* @param array $errors Validation errors generated by a plugin.
*/
public function add_errors(array $errors): void {
$this->errors = array_merge($this->errors, $errors);
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\hook;
use stdClass;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Hook before course deletion.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions before a course is deleted.')]
#[\core\attribute\tags('course')]
class before_course_deleted implements
StoppableEventInterface
{
use \core\hook\stoppable_trait;
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
) {
}
}
+481
View File
@@ -0,0 +1,481 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class core_course_list_element
*
* @package core
* @subpackage course
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Class to store information about one course in a list of courses
*
* Not all information may be retrieved when object is created but
* it will be retrieved on demand when appropriate property or method is
* called.
*
* Instances of this class are usually returned by functions
* {@link core_course_category::search_courses()}
* and
* {@link core_course_category::get_courses()}
*
* @property-read int $id
* @property-read int $category Category ID
* @property-read int $sortorder
* @property-read string $fullname
* @property-read string $shortname
* @property-read string $idnumber
* @property-read string $summary Course summary. Field is present if core_course_category::get_courses()
* was called with option 'summary'. Otherwise will be retrieved from DB on first request
* @property-read int $summaryformat Summary format. Field is present if core_course_category::get_courses()
* was called with option 'summary'. Otherwise will be retrieved from DB on first request
* @property-read string $format Course format. Retrieved from DB on first request
* @property-read int $showgrades Retrieved from DB on first request
* @property-read int $newsitems Retrieved from DB on first request
* @property-read int $startdate
* @property-read int $enddate
* @property-read int $marker Retrieved from DB on first request
* @property-read int $maxbytes Retrieved from DB on first request
* @property-read int $legacyfiles Retrieved from DB on first request
* @property-read int $showreports Retrieved from DB on first request
* @property-read int $visible
* @property-read int $visibleold Retrieved from DB on first request
* @property-read int $groupmode Retrieved from DB on first request
* @property-read int $groupmodeforce Retrieved from DB on first request
* @property-read int $defaultgroupingid Retrieved from DB on first request
* @property-read string $lang Retrieved from DB on first request
* @property-read string $theme Retrieved from DB on first request
* @property-read int $timecreated Retrieved from DB on first request
* @property-read int $timemodified Retrieved from DB on first request
* @property-read int $requested Retrieved from DB on first request
* @property-read int $enablecompletion Retrieved from DB on first request
* @property-read int $completionnotify Retrieved from DB on first request
* @property-read int $cacherev
*
* @package core
* @subpackage course
* @copyright 2013 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_list_element implements IteratorAggregate {
/** @var stdClass record retrieved from DB, may have additional calculated property such as managers and hassummary */
protected $record;
/** @var array array of course contacts - stores result of call to get_course_contacts() */
protected $coursecontacts;
/** @var bool true if the current user can access the course, false otherwise. */
protected $canaccess = null;
/**
* Creates an instance of the class from record
*
* @param stdClass $record except fields from course table it may contain
* field hassummary indicating that summary field is not empty.
* Also it is recommended to have context fields here ready for
* context preloading
*/
public function __construct(stdClass $record) {
context_helper::preload_from_record($record);
$this->record = new stdClass();
foreach ($record as $key => $value) {
$this->record->$key = $value;
}
}
/**
* Indicates if the course has non-empty summary field
*
* @return bool
*/
public function has_summary() {
if (isset($this->record->hassummary)) {
return !empty($this->record->hassummary);
}
if (!isset($this->record->summary)) {
// We need to retrieve summary.
$this->__get('summary');
}
return !empty($this->record->summary);
}
/**
* Indicates if the course have course contacts to display
*
* @return bool
*/
public function has_course_contacts() {
if (!isset($this->record->managers)) {
$courses = array($this->id => &$this->record);
core_course_category::preload_course_contacts($courses);
}
return !empty($this->record->managers);
}
/**
* Returns list of course contacts (usually teachers) to display in course link
*
* Roles to display are set up in $CFG->coursecontact
*
* The result is the list of users where user id is the key and the value
* is an array with elements:
* - 'user' - object containing basic user information
* - 'role' - object containing basic role information (id, name, shortname, coursealias)
* - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS)
* - 'username' => fullname($user, $canviewfullnames)
*
* @return array
*/
public function get_course_contacts() {
global $CFG;
if (empty($CFG->coursecontact)) {
// No roles are configured to be displayed as course contacts.
return array();
}
if (!$this->has_course_contacts()) {
// No course contacts exist.
return array();
}
if ($this->coursecontacts === null) {
$this->coursecontacts = array();
$context = context_course::instance($this->id);
$canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
$displayall = get_config('core', 'coursecontactduplicates');
foreach ($this->record->managers as $ruser) {
$processed = array_key_exists($ruser->id, $this->coursecontacts);
if (!$displayall && $processed) {
continue;
}
$role = (object)[
'id' => $ruser->roleid,
'name' => $ruser->rolename,
'shortname' => $ruser->roleshortname,
'coursealias' => $ruser->rolecoursealias,
];
$role->displayname = role_get_name($role, $context, ROLENAME_ALIAS);
if (!$processed) {
$user = username_load_fields_from_object((object)[], $ruser, null, ['id', 'username']);
$this->coursecontacts[$ruser->id] = [
'user' => $user,
'username' => fullname($user, $canviewfullnames),
// List of all roles.
'roles' => [],
// Primary role of this user.
'role' => $role,
'rolename' => $role->displayname,
];
}
$this->coursecontacts[$ruser->id]['roles'][$ruser->roleid] = $role;
}
}
return $this->coursecontacts;
}
/**
* Returns custom fields data for this course
*
* @return \core_customfield\data_controller[]
*/
public function get_custom_fields(): array {
if (!isset($this->record->customfields)) {
$this->record->customfields = \core_course\customfield\course_handler::create()->get_instance_data($this->id);
}
return $this->record->customfields;
}
/**
* Does this course have custom fields
*
* @return bool
*/
public function has_custom_fields(): bool {
$customfields = $this->get_custom_fields();
return !empty($customfields);
}
/**
* Checks if course has any associated overview files
*
* @return bool
*/
public function has_course_overviewfiles() {
global $CFG;
if (empty($CFG->courseoverviewfileslimit)) {
return false;
}
$fs = get_file_storage();
$context = context_course::instance($this->id);
return !$fs->is_area_empty($context->id, 'course', 'overviewfiles');
}
/**
* Returns all course overview files
*
* @return array array of stored_file objects
*/
public function get_course_overviewfiles() {
global $CFG;
if (empty($CFG->courseoverviewfileslimit)) {
return array();
}
require_once($CFG->libdir. '/filestorage/file_storage.php');
require_once($CFG->dirroot. '/course/lib.php');
$fs = get_file_storage();
$context = context_course::instance($this->id);
$files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false);
if (count($files)) {
$overviewfilesoptions = course_overviewfiles_options($this->id);
$acceptedtypes = $overviewfilesoptions['accepted_types'];
if ($acceptedtypes !== '*') {
// Filter only files with allowed extensions.
require_once($CFG->libdir. '/filelib.php');
foreach ($files as $key => $file) {
if (!file_extension_in_typegroup($file->get_filename(), $acceptedtypes)) {
unset($files[$key]);
}
}
}
if (count($files) > $CFG->courseoverviewfileslimit) {
// Return no more than $CFG->courseoverviewfileslimit files.
$files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true);
}
}
return $files;
}
/**
* Magic method to check if property is set
*
* @param string $name
* @return bool
*/
public function __isset($name) {
return isset($this->record->$name);
}
/**
* Magic method to get a course property
*
* Returns any field from table course (retrieves it from DB if it was not retrieved before)
*
* @param string $name
* @return mixed
*/
public function __get($name) {
global $DB;
if (property_exists($this->record, $name)) {
return $this->record->$name;
} else if ($name === 'summary' || $name === 'summaryformat') {
// Retrieve fields summary and summaryformat together because they are most likely to be used together.
$record = $DB->get_record('course', array('id' => $this->record->id), 'summary, summaryformat', MUST_EXIST);
$this->record->summary = $record->summary;
$this->record->summaryformat = $record->summaryformat;
return $this->record->$name;
} else if (array_key_exists($name, $DB->get_columns('course'))) {
// Another field from table 'course' that was not retrieved.
$this->record->$name = $DB->get_field('course', $name, array('id' => $this->record->id), MUST_EXIST);
return $this->record->$name;
}
debugging('Invalid course property accessed! '.$name);
return null;
}
/**
* All properties are read only, sorry.
*
* @param string $name
*/
public function __unset($name) {
debugging('Can not unset '.get_class($this).' instance properties!');
}
/**
* Magic setter method, we do not want anybody to modify properties from the outside
*
* @param string $name
* @param mixed $value
*/
public function __set($name, $value) {
debugging('Can not change '.get_class($this).' instance properties!');
}
/**
* Create an iterator because magic vars can't be seen by 'foreach'.
* Exclude context fields
*
* Implementing method from interface IteratorAggregate
*
* @return ArrayIterator
*/
public function getIterator(): Traversable {
$ret = array('id' => $this->record->id);
foreach ($this->record as $property => $value) {
$ret[$property] = $value;
}
return new ArrayIterator($ret);
}
/**
* Returns the name of this course as it should be displayed within a list.
* @return string
*/
public function get_formatted_name() {
return format_string(
get_course_display_name_for_list($this),
true,
['context' => $this->get_context()],
);
}
/**
* Returns the formatted fullname for this course.
* @return string
*/
public function get_formatted_fullname() {
return format_string(
$this->__get('fullname'),
true,
['context' => $this->get_context()],
);
}
/**
* Returns the formatted shortname for this course.
* @return string
*/
public function get_formatted_shortname() {
return format_string(
$this->__get('shortname'),
true,
['context' => $this->get_context()],
);
}
/**
* Returns true if the current user can access this course.
* @return bool
*/
public function can_access() {
if ($this->canaccess === null) {
$this->canaccess = can_access_course($this->record);
}
return $this->canaccess;
}
/**
* Returns true if the user can edit this courses settings.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_edit() {
return has_capability('moodle/course:update', $this->get_context());
}
/**
* Returns true if the user can change the visibility of this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_change_visibility() {
// You must be able to both hide a course and view the hidden course.
return has_all_capabilities(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'),
$this->get_context());
}
/**
* Returns the context for this course.
* @return context_course
*/
public function get_context() {
return context_course::instance($this->__get('id'));
}
/**
* Returns true if the current user can review enrolments for this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_review_enrolments() {
return has_capability('moodle/course:enrolreview', $this->get_context());
}
/**
* Returns true if the current user can delete this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_delete() {
return can_delete_course($this->id);
}
/**
* Returns true if the current user can backup this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_backup() {
return has_capability('moodle/backup:backupcourse', $this->get_context());
}
/**
* Returns true if the current user can restore this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_restore() {
return has_capability('moodle/restore:restorecourse', $this->get_context());
}
}
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Activity Chooser footer data class.
*
* @package core
* @subpackage course
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
/**
* A class to represent the Activity Chooser footer data.
*
* @package core
* @subpackage course
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_chooser_footer {
/** @var string $footerjspath The path to the plugin JS file to dynamically import later. */
protected $footerjspath;
/** @var string $footertemplate The rendered template for the footer. */
protected $footertemplate;
/** @var string $carouseltemplate The rendered template for the footer. */
protected $carouseltemplate;
/**
* Constructor method.
*
* @param string $footerjspath JS file to dynamically import later.
* @param string $footertemplate Footer template that has been rendered.
* @param string|null $carouseltemplate Carousel template that may have been rendered.
*/
public function __construct(string $footerjspath, string $footertemplate, ?string $carouseltemplate = '') {
$this->footerjspath = $footerjspath;
$this->footertemplate = $footertemplate;
$this->carouseltemplate = $carouseltemplate;
}
/**
* Get the footer JS file path for this plugin.
*
* @return string The JS file to call functions from.
*/
public function get_footer_js_file(): string {
return $this->footerjspath;
}
/**
* Get the footer rendered template for this plugin.
*
* @return string The template that has been rendered for the chooser footer.
*/
public function get_footer_template(): string {
return $this->footertemplate;
}
/**
* Get the carousel rendered template for this plugin.
*
* @return string The template that has been rendered for the chooser carousel.
*/
public function get_carousel_template(): string {
return $this->carouseltemplate;
}
}
@@ -0,0 +1,182 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the content_item class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The content_item class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item {
/** @var int $id the id. */
private $id;
/** @var string $name the name. */
private $name;
/** @var title $title the title. */
private $title;
/** @var \moodle_url $link the url for the content item's setup page (usually mod/edit.php). */
private $link;
/** @var string $icon an html string containing the icon for this item. */
private $icon;
/** @var string $help the description/help text for this content item. */
private $help;
/** @var int $achetype a module archetype, e.g. MOD_ARCHETYPE_RESOURCE, MOD_ARCHETYPE_OTHER. */
private $archetype;
/** @var string $componentname the name of the component from which this content item originates. */
private $componentname;
/** @var string $purpose the purpose type of this component. */
private $purpose;
/** @var bool $branded whether or not this component is branded. */
private $branded;
/**
* The content_item constructor.
*
* @param int $id Id number.
* @param string $name Name of the item, not human readable.
* @param title $title Human readable title for the item.
* @param \moodle_url $link The URL to the creation page, with any item specific params
* @param string $icon HTML containing the icon for the item
* @param string $help The description of the item.
* @param int $archetype the archetype for the content item (see MOD_ARCHETYPE_X definitions in lib/moodlelib.php).
* @param string $componentname the name of the component/plugin with which this content item is associated.
* @param string $purpose the purpose type of this component.
* @param bool $branded whether or not this item is branded.
*/
public function __construct(int $id, string $name, title $title, \moodle_url $link, string $icon, string $help,
int $archetype, string $componentname, string $purpose, bool $branded = false) {
$this->id = $id;
$this->name = $name;
$this->title = $title;
$this->link = $link;
$this->icon = $icon;
$this->help = $help;
$this->archetype = $archetype;
$this->componentname = $componentname;
$this->purpose = $purpose;
$this->branded = $branded;
}
/**
* Get the name of the component with which this content item is associated.
*
* @return string
*/
public function get_component_name(): string {
return $this->componentname;
}
/**
* Get the help description of this item.
*
* @return string
*/
public function get_help(): string {
return $this->help;
}
/**
* Get the archetype of this item.
*
* @return int
*/
public function get_archetype(): int {
return $this->archetype;
}
/**
* Get the id of this item.
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the name of this item.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* Get the human readable title of this item.
*
* @return title
*/
public function get_title(): title {
return $this->title;
}
/**
* Get the link to the creation page of this item.
*
* @return \moodle_url
*/
public function get_link(): \moodle_url {
return $this->link;
}
/**
* Get the icon html for this item.
*
* @return string
*/
public function get_icon(): string {
return $this->icon;
}
/**
* Get purpose for this item.
*
* @return string
*/
public function get_purpose(): string {
return $this->purpose;
}
/**
* Whether this item is branded.
*
* @return bool true if this item is branded, false otherwise.
*/
public function is_branded(): bool {
return $this->branded;
}
}
@@ -0,0 +1,62 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the lang_string_title class of value object, providing access to the value of a lang string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The lang_string_title class of value object, providing access to the value of a lang string.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lang_string_title implements title {
/** @var string $component the component name. */
private $component;
/** @var string $identifier the string identifier. */
private $identifier;
/**
* The lang_string_title constructor.
*
* @param string $identifier the component name.
* @param string $component the string identifier.
*/
public function __construct(string $identifier, string $component) {
$this->identifier = $identifier;
$this->component = $component;
}
/**
* Returns the value of the wrapped string.
*
* @return string the value of the string.
*/
public function get_value(): string {
return get_string($this->identifier, $this->component);
}
}
@@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the string_title class of value object, which provides access to a simple string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The string_title class of value object, which provides access to a simple string.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class string_title implements title {
/** @var string $title the title string. */
private $title;
/**
* The string_title constructor.
*
* @param string $title a string.
*/
public function __construct(string $title) {
$this->title = $title;
}
/**
* Return the value of the wrapped string.
*
* @return string
*/
public function get_value(): string {
return $this->title;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the title value object interface, which provides a basic interface to a string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
interface title {
/**
* Get the value of this title.
*/
public function get_value(): string;
}
@@ -0,0 +1,161 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the course_content_item_exporter class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\exporters;
defined('MOODLE_INTERNAL') || die();
use core\external\exporter;
use core_course\local\entity\content_item;
use core_course\local\service\content_item_service;
/**
* The course_content_item_exporter class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_content_item_exporter extends exporter {
/** @var content_item $contentitem the content_item to export. */
private $contentitem;
/**
* The course_content_item_exporter constructor.
*
* @param content_item $contentitem the content item to export.
* @param array $related the array of related objects used during export.
*/
public function __construct(content_item $contentitem, array $related = []) {
$this->contentitem = $contentitem;
return parent::__construct([], $related);
}
/**
* Definition of all properties originating in the export target, \core_course\local\entity\content_item.
*
* @return array The array of property values, indexed by name.
*/
protected static function define_properties() {
return [
'id' => ['type' => PARAM_INT, 'description' => 'The id of the content item'],
'name' => ['type' => PARAM_TEXT, 'description' => 'Name of the content item'],
'title' => ['type' => PARAM_TEXT, 'description' => 'The string title of the content item, human readable'],
'link' => ['type' => PARAM_URL, 'description' => 'The link to the content item creation page'],
'icon' => ['type' => PARAM_RAW, 'description' => 'Html containing the icon for the content item'],
'help' => ['type' => PARAM_RAW, 'description' => 'Html description / help for the content item'],
'archetype' => ['type' => PARAM_RAW, 'description' => 'The archetype of the module exposing the content item'],
'componentname' => ['type' => PARAM_TEXT, 'description' => 'The name of the component exposing the content item'],
'purpose' => ['type' => PARAM_TEXT, 'description' => 'The purpose of the component exposing the content item'],
'branded' => ['type' => PARAM_BOOL, 'description' => ' Whether this content item is branded or not'],
];
}
/**
* Definition of all properties which are either calculated or originate in a related domain object.
*
* @return array The array of property values, indexed by name.
*/
protected static function define_other_properties() {
// This will hold user-dependant properties such as whether the item is starred or recommended.
return [
'favourite' => ['type' => PARAM_BOOL, 'description' => 'Has the user favourited the content item'],
'legacyitem' => [
'type' => PARAM_BOOL,
'description' => 'If this item was pulled from the old callback and has no item id.'
],
'recommended' => ['type' => PARAM_BOOL, 'description' => 'Has this item been recommended'],
];
}
/**
* Get ALL properties for the content_item DTO being exported.
*
* These properties are a mix of:
* - readonly properties of the primary object (content_item) being exported.
* - calculated values
* - properties originating from the related domain objects.
*
* Normally, those properties defined in get_properties() are added to the export automatically as part of the superclass code,
* provided they are public properties on the export target. In this case, the export target is content_item, which doesn't
* provide public access to its properties, so those are fetched via their respective getters here.
*
* @param \renderer_base $output
* @return array The array of property values, indexed by name.
*/
protected function get_other_values(\renderer_base $output) {
$favourite = false;
$itemtype = 'contentitem_' . $this->contentitem->get_component_name();
if (isset($this->related['favouriteitems'])) {
foreach ($this->related['favouriteitems'] as $favobj) {
if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) {
$favourite = true;
}
}
}
$recommended = false;
$itemtype = content_item_service::RECOMMENDATION_PREFIX . $this->contentitem->get_component_name();
if (isset($this->related['recommended'])) {
foreach ($this->related['recommended'] as $favobj) {
if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) {
$recommended = true;
}
}
}
$properties = [
'id' => $this->contentitem->get_id(),
'name' => $this->contentitem->get_name(),
'title' => $this->contentitem->get_title()->get_value(),
'link' => $this->contentitem->get_link()->out(false),
'icon' => $this->contentitem->get_icon(),
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN),
'archetype' => $this->contentitem->get_archetype(),
'componentname' => $this->contentitem->get_component_name(),
'favourite' => $favourite,
'legacyitem' => ($this->contentitem->get_id() == -1),
'recommended' => $recommended,
'purpose' => $this->contentitem->get_purpose(),
'branded' => $this->contentitem->is_branded(),
];
return $properties;
}
/**
* Define the list of related objects, used by this exporter.
*
* @return array the list of related objects.
*/
protected static function define_related(): array {
return [
'context' => '\context',
'favouriteitems' => '\stdClass[]?',
'recommended' => '\stdClass[]?'
];
}
}
@@ -0,0 +1,108 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the course_content_items_exporter class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\exporters;
defined('MOODLE_INTERNAL') || die();
use core\external\exporter;
use core_course\local\entity\content_item;
/**
* The course_content_items_exporter class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_content_items_exporter extends exporter {
/** @var content_item[] the array of content items. */
private $contentitems;
/**
* The course_content_items_exporter constructor.
*
* @param array $contentitems the array of \core_course\local\entity\content_item objects to export.
* @param array $related any related objects, see define_related for what's expected.
*/
public function __construct(array $contentitems, array $related) {
$this->contentitems = $contentitems;
parent::__construct([], $related);
}
/**
* Return the properties defining this export.
*
* @return array the array of properties.
*/
public static function define_properties() {
return [
'content_items' => [
'type' => course_content_item_exporter::read_properties_definition(),
'multiple' => true
]
];
}
/**
* Generate and return the data for this export.
*
* @param \renderer_base $output
* @return array the array of course content_items
*/
protected function get_other_values(\renderer_base $output) {
$contentitemexport = function(content_item $contentitem) use ($output) {
$exporter = new course_content_item_exporter(
$contentitem,
[
'context' => $this->related['context'],
'favouriteitems' => $this->related['favouriteitems'],
'recommended' => $this->related['recommended']
]
);
return $exporter->export($output);
};
$exportedcontentitems = array_map($contentitemexport, $this->contentitems);
return [
'content_items' => $exportedcontentitems
];
}
/**
* Define the list of related objects, used by this exporter.
*
* @return array the list of related objects.
*/
protected static function define_related() {
return [
'context' => '\context',
'favouriteitems' => '\stdClass[]?',
'recommended' => '\stdClass[]?'
];
}
}
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the service_factory, a locator for services for course content items.
*
* Services encapsulate the business logic, and any data manipulation code, and are what clients should interact with.
*
* @package core_course
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\factory;
defined('MOODLE_INTERNAL') || die();
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use core_course\local\service\content_item_service;
/**
* Class service_factory, providing functions for location of service objects for course content items.
*
* This class is responsible for providing service objects to clients only.
*
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_service_factory {
/**
* Returns a basic service object providing operations for course content items.
*
* @return content_item_service
*/
public static function get_content_item_service(): content_item_service {
return new content_item_service(
new caching_content_item_readonly_repository(
\cache::make('core', 'user_course_content_items'),
new content_item_readonly_repository()
)
);
}
}
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class caching_content_item_repository, for fetching content_items, with additional caching.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
/**
* The class caching_content_item_repository, for fetching content_items, with additional caching.
*
* This class decorates the content_item_repository and uses the supplied cache to store content items for user and course
* combinations. The content items for subsequent calls are returned from the cache if present, else are retrieved from the wrapped
* content_item_repository.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class caching_content_item_readonly_repository implements content_item_readonly_repository_interface {
/** @var \cache $cachestore the cache to use. */
private $cachestore;
/** @var content_item_readonly_repository $contentitemrepository a content item repository. */
private $contentitemrepository;
/**
* The caching_content_item_readonly_repository constructor.
*
* @param \cache $cachestore a cache to use.
* @param content_item_readonly_repository $contentitemrepository the repository to use as a fallback, after a cache miss.
*/
public function __construct(\cache $cachestore, content_item_readonly_repository $contentitemrepository) {
$this->cachestore = $cachestore;
$this->contentitemrepository = $contentitemrepository;
}
/**
* Find all the content items for a given course and user.
*
* @param \stdClass $course The course to find content items for.
* @param \stdClass $user the user to pass to plugins.
* @return array the array of content items.
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array {
global $USER;
// Try to find this data in the cache first.
$key = $USER->id . '_' . $course->id;
$contentitems = $this->cachestore->get($key);
if ($contentitems !== false) {
return $contentitems;
}
// If we can't find it there, we must get it from the slow data store, updating the cache in the process.
$contentitems = $this->contentitemrepository->find_all_for_course($course, $user);
$this->cachestore->set($key, $contentitems);
return $contentitems;
}
/**
* Find all the content items made available by core and plugins.
*
* @return array
*/
public function find_all(): array {
return $this->contentitemrepository->find_all();
}
}
@@ -0,0 +1,246 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class content_item_repository, for fetching content_items.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
use core_component;
use core_course\local\entity\content_item;
use core_course\local\entity\lang_string_title;
/**
* The class content_item_repository, for reading content_items.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_readonly_repository implements content_item_readonly_repository_interface {
/**
* Get the help string for content items representing core modules.
*
* @param string $modname the module name.
* @return string the help string, including help link.
*/
private function get_core_module_help_string(string $modname): string {
global $OUTPUT;
$help = '';
$sm = get_string_manager();
if ($sm->string_exists('modulename_help', $modname)) {
$help = get_string('modulename_help', $modname);
if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs.
$link = get_string('modulename_link', $modname);
$linktext = get_string('morehelp');
$arialabel = get_string('morehelpaboutmodule', '', get_string('modulename', $modname));
$doclink = $OUTPUT->doc_link($link, $linktext, true, ['aria-label' => $arialabel]);
$help .= \html_writer::tag('div', $doclink, ['class' => 'helpdoclink']);
}
}
return $help;
}
/**
* Helper to get the contentitems from all subplugin hooks for a given module plugin.
*
* @param string $parentpluginname the name of the module plugin to check subplugins for.
* @param content_item $modulecontentitem the content item of the module plugin, to pass to the hooks.
* @param \stdClass $user the user object to pass to subplugins.
* @return array the array of content items.
*/
private function get_subplugin_course_content_items(string $parentpluginname, content_item $modulecontentitem,
\stdClass $user): array {
$contentitems = [];
$pluginmanager = \core_plugin_manager::instance();
foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
// Call the hook, but with a copy of the module content item data.
$spcontentitems = component_callback($subpluginname, 'get_course_content_items', [$modulecontentitem, $user], null);
if (!is_null($spcontentitems)) {
foreach ($spcontentitems as $spcontentitem) {
$contentitems[] = $spcontentitem;
}
}
}
return $contentitems;
}
/**
* Get all the content items for a subplugin.
*
* @param string $parentpluginname
* @param content_item $modulecontentitem
* @return array
*/
private function get_subplugin_all_content_items(string $parentpluginname, content_item $modulecontentitem): array {
$contentitems = [];
$pluginmanager = \core_plugin_manager::instance();
foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
// Call the hook, but with a copy of the module content item data.
$spcontentitems = component_callback($subpluginname, 'get_all_content_items', [$modulecontentitem], null);
if (!is_null($spcontentitems)) {
foreach ($spcontentitems as $spcontentitem) {
$contentitems[] = $spcontentitem;
}
}
}
return $contentitems;
}
/**
* Find all the available content items, not restricted to course or user.
*
* @return array the array of content items.
*/
public function find_all(): array {
global $OUTPUT, $DB, $CFG;
// Get all modules so we know which plugins are enabled and able to add content.
// Only module plugins may add content items.
$modules = $DB->get_records('modules', ['visible' => 1]);
$return = [];
// Now, generate the content_items.
foreach ($modules as $modid => $mod) {
// Exclude modules if the code doesn't exist.
if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
continue;
}
// Create the content item for the module itself.
// If the module chooses to implement the hook, this may be thrown away.
$help = $this->get_core_module_help_string($mod->name);
$archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
$purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER);
$isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false);
$contentitem = new content_item(
$mod->id,
$mod->name,
new lang_string_title("modulename", $mod->name),
new \moodle_url(''), // No course scope, so just an empty link.
$OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']),
$help,
$archetype,
'mod_' . $mod->name,
$purpose,
$isbranded,
);
$modcontentitemreference = clone($contentitem);
if (component_callback_exists('mod_' . $mod->name, 'get_all_content_items')) {
// Call the module hooks for this module.
$plugincontentitems = component_callback('mod_' . $mod->name, 'get_all_content_items',
[$modcontentitemreference], []);
if (!empty($plugincontentitems)) {
array_push($return, ...$plugincontentitems);
}
// Now, get those for subplugins of the module.
$subplugincontentitems = $this->get_subplugin_all_content_items('mod_' . $mod->name, $modcontentitemreference);
if (!empty($subplugincontentitems)) {
array_push($return, ...$subplugincontentitems);
}
} else {
// Neither callback was found, so just use the default module content item.
$return[] = $contentitem;
}
}
return $return;
}
/**
* Get the list of potential content items for the given course.
*
* @param \stdClass $course the course
* @param \stdClass $user the user, to pass to plugins implementing callbacks.
* @return array the array of content_item objects
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array {
global $OUTPUT, $DB, $CFG;
// Get all modules so we know which plugins are enabled and able to add content.
// Only module plugins may add content items.
$modules = $DB->get_records('modules', ['visible' => 1]);
$return = [];
// Now, generate the content_items.
foreach ($modules as $modid => $mod) {
// Exclude modules if the code doesn't exist.
if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
continue;
}
// Create the content item for the module itself.
// If the module chooses to implement the hook, this may be thrown away.
$help = $this->get_core_module_help_string($mod->name);
$archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
$purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER);
$isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false);
$icon = 'monologo';
// Quick check for monologo icons.
// Plugins that don't have monologo icons will be displayed as is and CSS filter will not be applied.
$hasmonologoicons = core_component::has_monologo_icon('mod', $mod->name);
$iconclass = '';
if (!$hasmonologoicons) {
$iconclass = 'nofilter';
}
$contentitem = new content_item(
$mod->id,
$mod->name,
new lang_string_title("modulename", $mod->name),
new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]),
$OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]),
$help,
$archetype,
'mod_' . $mod->name,
$purpose,
$isbranded,
);
$modcontentitemreference = clone($contentitem);
if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
// Call the module hooks for this module.
$plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
[$modcontentitemreference, $user, $course], []);
if (!empty($plugincontentitems)) {
array_push($return, ...$plugincontentitems);
}
// Now, get those for subplugins of the module.
$subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
if (!empty($subpluginitems)) {
array_push($return, ...$subpluginitems);
}
} else {
// Callback was not found, so just use the default module content item.
$return[] = $contentitem;
}
}
return $return;
}
}
@@ -0,0 +1,48 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the interface content_item_readonly_repository_interface, defining operations for readonly content item repositories.
*
* This interface is not considered a published interface and serves to govern internal, local repository objects only.
* All calling code should use instances of the service classes, and should not interact with repositories directly.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
interface content_item_readonly_repository_interface {
/**
* Find all content items for a given course and user.
*
* @param \stdClass $course the course object.
* @param \stdClass $user the user object.
* @return array the array of content items.
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array;
/**
* Find all content items that can be presented, irrespective of course.
*
* @return array the array of content items.
*/
public function find_all(): array;
}
@@ -0,0 +1,386 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the content_item_service class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\service;
defined('MOODLE_INTERNAL') || die();
use core_course\local\exporters\course_content_items_exporter;
use core_course\local\repository\content_item_readonly_repository_interface;
/**
* The content_item_service class, providing the api for interacting with content items.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_service {
/** @var content_item_readonly_repository_interface $repository a repository for content items. */
private $repository;
/** string the component for this favourite. */
public const COMPONENT = 'core_course';
/** string the favourite prefix itemtype in the favourites table. */
public const FAVOURITE_PREFIX = 'contentitem_';
/** string the recommendation prefix itemtype in the favourites table. */
public const RECOMMENDATION_PREFIX = 'recommend_';
/** string the cache name for recommendations. */
public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items';
/**
* The content_item_service constructor.
*
* @param content_item_readonly_repository_interface $repository a content item repository.
*/
public function __construct(content_item_readonly_repository_interface $repository) {
$this->repository = $repository;
}
/**
* Returns an array of objects representing favourited content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @param \stdClass $user
* @return array
*/
private function get_favourite_content_items_for_user(\stdClass $user): array {
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$key = $user->id;
$favmods = $favcache->get($key);
if ($favmods !== false) {
return $favmods;
}
$favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));
$favcache->set($key, $favourites);
return $favourites;
}
/**
* Returns an array of objects representing recommended content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @return array
*/
private function get_recommendations(): array {
global $CFG;
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$key = $CFG->siteguest;
$favmods = $recommendationcache->get($key);
if ($favmods !== false) {
return $favmods;
}
// Make sure the guest user exists in the database.
if (!\core_user::get_user($CFG->siteguest)) {
throw new \coding_exception('The guest user does not exist in the database.');
}
// Make sure the guest user context exists.
if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) {
throw new \coding_exception('The guest user context does not exist.');
}
$favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);
$recommendationcache->set($CFG->siteguest, $favourites);
return $favourites;
}
/**
* Gets content favourites from the favourites system depending on the area.
*
* @param string $prefix Prefix for the item type.
* @param \context_user $usercontext User context for the favourite
* @return array An array of favourite objects.
*/
private function get_content_favourites(string $prefix, \context_user $usercontext): array {
// Get all modules and any submodules which implement get_course_content_items() hook.
// This gives us the set of all itemtypes which we'll use to register favourite content items.
// The ids that each plugin returns will be used together with the itemtype to uniquely identify
// each content item for favouriting.
$pluginmanager = \core_plugin_manager::instance();
$plugins = $pluginmanager->get_plugins_of_type('mod');
$itemtypes = [];
foreach ($plugins as $plugin) {
// Add the mod itself.
$itemtypes[] = $prefix . 'mod_' . $plugin->name;
// Add any subplugins to the list of item types.
$subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
foreach ($subplugins as $subpluginname => $subplugininfo) {
try {
if (component_callback_exists($subpluginname, 'get_course_content_items')) {
$itemtypes[] = $prefix . $subpluginname;
}
} catch (\moodle_exception $e) {
debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
}
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
$favourites = [];
$favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
$favsreduced = array_reduce($favs, function($carry, $item) {
$carry[$item->itemtype][$item->itemid] = 0;
return $carry;
}, []);
foreach ($itemtypes as $type) {
$favourites[] = (object) [
'itemtype' => $type,
'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
];
}
return $favourites;
}
/**
* Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user the user object.
* @return array the array of exported content items.
*/
public function get_all_content_items(\stdClass $user): array {
$allcontentitems = $this->repository->find_all();
return $this->export_content_items($user, $allcontentitems);
}
/**
* Get content items which name matches a certain pattern and may be added to courses,
* irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user The user object.
* @param string $pattern The search pattern.
* @return array The array of exported content items.
*/
public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
$allcontentitems = $this->repository->find_all();
$filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
});
return $this->export_content_items($user, $filteredcontentitems);
}
/**
* Export content items.
*
* @param \stdClass $user The user object.
* @param array $contentitems The content items array.
* @return array The array of exported content items.
*/
private function export_content_items(\stdClass $user, $contentitems) {
global $PAGE;
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommendations = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$contentitems,
[
'context' => \context_system::instance(),
'favouriteitems' => $favourites,
'recommended' => $recommendations
]
);
$exported = $ciexporter->export($PAGE->get_renderer('core'));
// Sort by title for return.
\core_collator::asort_objects_by_property($exported->content_items, 'title');
return array_values($exported->content_items);
}
/**
* Return a representation of the available content items, for a user in a course.
*
* @param \stdClass $user the user to check access for.
* @param \stdClass $course the course to scope the content items to.
* @param array $linkparams the desired section to return to.
* @return \stdClass[] the content items, scoped to a course.
*/
public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
global $PAGE;
if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
return [];
}
// Get all the visible content items.
$allcontentitems = $this->repository->find_all_for_course($course, $user);
// Content items can only originate from modules or submodules.
$pluginmanager = \core_plugin_manager::instance();
$components = \core_component::get_component_list();
$parents = [];
foreach ($allcontentitems as $contentitem) {
if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
// It could be a subplugin.
$info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
if (!is_null($info)) {
$parent = $info->get_parent_plugin();
if ($parent != false) {
if (in_array($parent, array_keys($components['mod']))) {
$parents[$contentitem->get_component_name()] = $parent;
continue;
}
}
}
throw new \moodle_exception('Only modules and submodules can generate content items. \''
. $contentitem->get_component_name() . '\' is neither.');
}
$parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
}
// Now, check access to these items for the user.
$availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
// Check the parent module access for the user.
return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
});
// Add the link params to the link, if any have been provided.
if (!empty($linkparams)) {
$availablecontentitems = array_map(function ($item) use ($linkparams) {
$item->get_link()->params($linkparams);
return $item;
}, $availablecontentitems);
}
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommended = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$availablecontentitems,
[
'context' => \context_course::instance($course->id),
'favouriteitems' => $favourites,
'recommended' => $recommended
]
);
$exported = $ciexporter->export($PAGE->get_renderer('course'));
// Sort by title for return.
\core_collator::asort_objects_by_property($exported->content_items, 'title');
return array_values($exported->content_items);
}
/**
* Add a content item to a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Remove the content item from a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Toggle an activity to being recommended or not.
*
* @param string $itemtype The component such as mod_assign, or assignsubmission_file
* @param int $itemid The id related to this component item.
* @return bool True on creating a favourite, false on deleting it.
*/
public function toggle_recommendation(string $itemtype, int $itemid): bool {
global $CFG;
$context = \context_system::instance();
$itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
// Favourites are created using a user context. We'll use the site guest user ID as that should not change and there
// can be only one.
$usercontext = \context_user::instance($CFG->siteguest);
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) {
$favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return false;
} else {
$favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return true;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output;
use cm_info;
use core_availability\info;
use core_completion\cm_completion_details;
use core_user;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity completion renderable class.
*
* @package core_course
* @copyright 2023 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_completion implements renderable, templatable {
/**
* Constructor.
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $cmcompletion The course module information.
*/
public function __construct(
protected cm_info $cminfo,
protected cm_completion_details $cmcompletion
) {
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
global $CFG;
$overallcompletion = $this->cmcompletion->get_overall_completion();
$isoverallcomplete = $this->cmcompletion->is_overall_complete();
$overrideby = $this->get_overrideby();
$course = $this->cminfo->get_course();
// Whether the completion of this activity controls the availability of other activities/sections in the course.
// An activity with manual completion tracking which is used to enable access to other activities/sections in
// the course needs to refresh the page after having its completion state toggled. This withavailability flag will enable
// this functionality on the course homepage. Otherwise, the completion toggling will just happen normally via ajax.
if ($this->cmcompletion->has_completion() && $this->cmcompletion->is_manual()) {
$withavailability = !empty($CFG->enableavailability) && info::completion_value_used($course, $this->cminfo->id);
}
return (object) [
'cmid' => $this->cminfo->id,
'activityname' => $this->cminfo->get_formatted_name(),
'uservisible' => $this->cminfo->uservisible,
'hascompletion' => $this->cmcompletion->has_completion(),
'isautomatic' => $this->cmcompletion->is_automatic(),
'ismanual' => $this->cmcompletion->is_manual(),
'showmanualcompletion' => $this->cmcompletion->show_manual_completion(),
'istrackeduser' => $this->cmcompletion->is_tracked_user(),
'overallcomplete' => $isoverallcomplete,
'overallincomplete' => !$isoverallcomplete,
'withavailability' => $withavailability ?? false,
'overrideby' => $overrideby,
'completiondetails' => $this->get_completion_details($overrideby),
'accessibledescription' => $this->get_accessible_description($overrideby, $overallcompletion),
];
}
/**
* Returns the name of the user overriding the completion condition, if available.
*
* @return string
*/
private function get_overrideby(): string {
$overrideby = $this->cmcompletion->overridden_by();
if (!empty($overrideby)) {
$userfields = fields::for_name();
$overridebyrecord = core_user::get_user($overrideby, 'id ' . $userfields->get_sql()->selects, MUST_EXIST);
return fullname($overridebyrecord);
}
return '';
}
/**
* Returns automatic completion details
*
* @param string $overrideby The name of the user overriding the completion condition, if available.
* @return array
*/
private function get_completion_details($overrideby): array {
$details = [];
foreach ($this->cmcompletion->get_details() as $key => $detail) {
$detail->key = $key;
$detail->statuscomplete = in_array($detail->status, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
$detail->statuscompletefail = $detail->status == COMPLETION_COMPLETE_FAIL;
// This is not used by core themes but may be needed in custom themes.
$detail->statuscompletepass = $detail->status == COMPLETION_COMPLETE_PASS;
$detail->statusincomplete = $detail->status == COMPLETION_INCOMPLETE;
// Add an accessible description to be used for title and aria-label attributes for overridden completion details.
if ($overrideby) {
$setbydata = (object)[
'condition' => $detail->description,
'setby' => $overrideby,
];
$overridestatus = $detail->statuscomplete ? 'done' : 'todo';
$detail->accessibledescription = get_string('completion_setby:auto:' . $overridestatus, 'course', $setbydata);
}
unset($detail->status);
$details[] = $detail;
}
return $details;
}
/**
* Returns the accessible description for manual completions with overridden completion state.
*
* @param string $overrideby The name of the user overriding the completion condition, if available.
* @param int $overallcompletion The overall completion state of the activity.
* @return string
*/
private function get_accessible_description($overrideby, $overallcompletion): string {
if ($this->cmcompletion->is_manual() && $overrideby) {
$setbydata = (object)[
'activityname' => $this->cminfo->get_formatted_name(),
'setby' => $overrideby,
];
$isoverallcompleted = $overallcompletion == COMPLETION_COMPLETE;
$setbylangkey = $isoverallcompleted ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
return get_string($setbylangkey, 'course', $setbydata);
}
return '';
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity dates renderable class.
*
* @package core_course
* @copyright 2023 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_dates implements renderable, templatable {
/**
* Constructor.
*
* @param array $activitydates The activity dates.
*/
public function __construct(
protected array $activitydates
) {
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$activitydates = [];
foreach ($this->activitydates as $date) {
if (empty($date['relativeto'])) {
$date['datestring'] = userdate($date['timestamp'], get_string('strftimedaydatetime', 'core_langconfig'));
} else {
$diffstr = get_time_interval_string($date['timestamp'], $date['relativeto']);
if ($date['timestamp'] >= $date['relativeto']) {
$date['datestring'] = get_string('relativedatessubmissionduedateafter', 'core_course',
['datediffstr' => $diffstr]);
} else {
$date['datestring'] = get_string('relativedatessubmissionduedatebefore', 'core_course',
['datediffstr' => $diffstr]);
}
}
$activitydates[] = $date;
}
return (object) [
'hasdates' => !empty($this->activitydates),
'activitydates' => $activitydates,
];
}
}
@@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File containing the class activity information renderable.
*
* @package core_course
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
defined('MOODLE_INTERNAL') || die();
use cm_info;
use completion_info;
use context;
use core\activity_dates;
use core_availability\info;
use core_completion\cm_completion_details;
use core_user;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity information renderable class.
*
* @deprecated since Moodle 4.3 MDL-78744
* @todo MDL-78926 This class will be deleted in Moodle 4.7
*
* @package core_course
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_information implements renderable, templatable {
/** @var cm_info The course module information. */
protected $cminfo = null;
/** @var array The array of relevant dates for this activity. */
protected $activitydates = [];
/** @var cm_completion_details The user's completion details for this activity. */
protected $cmcompletion = null;
/**
* Constructor.
*
* @deprecated since Moodle 4.3
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $cmcompletion The course module information.
* @param array $activitydates The activity dates.
*/
public function __construct(cm_info $cminfo, cm_completion_details $cmcompletion, array $activitydates) {
debugging('activity_information class is deprecated. Use activity_completion and activity_dates instead.', DEBUG_DEVELOPER);
$this->cminfo = $cminfo;
$this->cmcompletion = $cmcompletion;
$this->activitydates = $activitydates;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @deprecated since Moodle 4.3
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
debugging('activity_information class is deprecated. Use activity_completion and activity_dates instead.', DEBUG_DEVELOPER);
$data = $this->build_completion_data();
$data->cmid = $this->cminfo->id;
$data->activityname = $this->cminfo->get_formatted_name();
$this->build_dates_data($data);
$data->hasdates = !empty($this->activitydates);
return $data;
}
/**
* Builds the dates data for export.
*
* @param stdClass $data
*/
protected function build_dates_data(stdClass $data): void {
foreach ($this->activitydates as $date) {
if (empty($date['relativeto'])) {
$date['datestring'] = userdate($date['timestamp'], get_string('strftimedaydatetime', 'core_langconfig'));
} else {
$diffstr = get_time_interval_string($date['timestamp'], $date['relativeto']);
if ($date['timestamp'] >= $date['relativeto']) {
$date['datestring'] = get_string('relativedatessubmissionduedateafter', 'core_course',
['datediffstr' => $diffstr]);
} else {
$date['datestring'] = get_string('relativedatessubmissionduedatebefore', 'core_course',
['datediffstr' => $diffstr]);
}
}
$data->activitydates[] = $date;
}
}
/**
* Builds the completion data for export.
*
* @return stdClass
*/
protected function build_completion_data(): stdClass {
global $CFG;
$data = new stdClass();
$data->hascompletion = $this->cmcompletion->has_completion();
$data->isautomatic = $this->cmcompletion->is_automatic();
$data->ismanual = $this->cmcompletion->is_manual();
$data->showmanualcompletion = $this->cmcompletion->show_manual_completion();
// Get the name of the user overriding the completion condition, if available.
$data->overrideby = null;
$overrideby = $this->cmcompletion->overridden_by();
$overridebyname = null;
if (!empty($overrideby)) {
$userfields = fields::for_name();
$overridebyrecord = core_user::get_user($overrideby, 'id ' . $userfields->get_sql()->selects, MUST_EXIST);
$data->overrideby = fullname($overridebyrecord);
}
// We'll show only the completion conditions and not the completion status if we're not tracking completion for this user
// (e.g. a teacher, admin).
$data->istrackeduser = $this->cmcompletion->is_tracked_user();
// Overall completion states.
$overallcompletion = $this->cmcompletion->get_overall_completion();
$data->overallcomplete = $overallcompletion == COMPLETION_COMPLETE;
$data->overallincomplete = $overallcompletion == COMPLETION_INCOMPLETE;
// Set an accessible description for manual completions with overridden completion state.
if (!$data->isautomatic && $data->overrideby) {
$setbydata = (object)[
'activityname' => $this->cminfo->get_formatted_name(),
'setby' => $data->overrideby,
];
$setbylangkey = $data->overallcomplete ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
$data->accessibledescription = get_string($setbylangkey, 'course', $setbydata);
}
// Whether the completion of this activity controls the availability of other activities/sections in the course.
$data->withavailability = false;
$course = $this->cminfo->get_course();
// An activity with manual completion tracking which is used to enable access to other activities/sections in
// the course needs to refresh the page after having its completion state toggled. This withavailability flag will enable
// this functionality on the course homepage. Otherwise, the completion toggling will just happen normally via ajax.
if ($this->cmcompletion->has_completion() && !$this->cmcompletion->is_automatic()) {
$data->withavailability = !empty($CFG->enableavailability) && info::completion_value_used($course, $this->cminfo->id);
}
// Whether this activity is visible to the user. If not, completion information will not be shown.
$data->uservisible = $this->cminfo->uservisible;
// Build automatic completion details.
$details = [];
foreach ($this->cmcompletion->get_details() as $key => $detail) {
// Set additional attributes for the template.
$detail->key = $key;
$detail->statuscomplete = in_array($detail->status, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
$detail->statuscompletefail = $detail->status == COMPLETION_COMPLETE_FAIL;
// This is not used by core themes but may be needed in custom themes.
$detail->statuscompletepass = $detail->status == COMPLETION_COMPLETE_PASS;
$detail->statusincomplete = $detail->status == COMPLETION_INCOMPLETE;
// Add an accessible description to be used for title and aria-label attributes for overridden completion details.
if ($data->overrideby) {
$setbydata = (object)[
'condition' => $detail->description,
'setby' => $data->overrideby,
];
$overridestatus = $detail->statuscomplete ? 'done' : 'todo';
$detail->accessibledescription = get_string('completion_setby:auto:' . $overridestatus, 'course', $setbydata);
}
// We don't need the status in the template.
unset($detail->status);
$details[] = $detail;
}
$data->completiondetails = $details;
return $data;
}
}
@@ -0,0 +1,127 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File containing the class activity navigation renderable.
*
* @package core_course
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
use url_select;
/**
* The class activity navigation renderable.
*
* @package core_course
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_navigation implements renderable, templatable {
/**
* @var \action_link The action link object for the prev link.
*/
public $prevlink = null;
/**
* @var \action_link The action link object for the next link.
*/
public $nextlink = null;
/**
* @var url_select The url select object for the activity selector menu.
*/
public $activitylist = null;
/**
* Constructor.
*
* @param \cm_info|null $prevmod The previous module to display, null if none.
* @param \cm_info|null $nextmod The next module to display, null if none.
* @param array $activitylist The list of activity URLs (as key) and names (as value) for the activity dropdown menu.
*/
public function __construct($prevmod, $nextmod, $activitylist = array()) {
global $OUTPUT;
// Check if there is a previous module to display.
if ($prevmod) {
$linkurl = new \moodle_url($prevmod->url, array('forceview' => 1));
$linkname = $prevmod->get_formatted_name();
if (!$prevmod->visible) {
$linkname .= ' ' . get_string('hiddenwithbrackets');
}
$attributes = [
'class' => 'btn btn-link',
'id' => 'prev-activity-link',
];
$this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
}
// Check if there is a next module to display.
if ($nextmod) {
$linkurl = new \moodle_url($nextmod->url, array('forceview' => 1));
$linkname = $nextmod->get_formatted_name();
if (!$nextmod->visible) {
$linkname .= ' ' . get_string('hiddenwithbrackets');
}
$attributes = [
'class' => 'btn btn-link',
'id' => 'next-activity-link',
];
$this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
}
// Render the activity list dropdown menu if available.
if (!empty($activitylist)) {
$select = new url_select($activitylist, '', array('' => get_string('jumpto')));
$select->set_label(get_string('jumpto'), array('class' => 'sr-only'));
$select->attributes = array('id' => 'jump-to-activity');
$this->activitylist = $select;
}
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \renderer_base $output Renderer base.
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
$data = new \stdClass();
if ($this->prevlink) {
$data->prevlink = $this->prevlink->export_for_template($output);
}
if ($this->nextlink) {
$data->nextlink = $this->nextlink->export_for_template($output);
}
if ($this->activitylist) {
$data->activitylist = $this->activitylist->export_for_template($output);
}
return $data;
}
}
@@ -0,0 +1,154 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use core_completion\manager;
defined('MOODLE_INTERNAL') || die;
require_once($CFG->dirroot.'/course/renderer.php');
/**
* Main renderer for the bulk activity completion stuff.
*
* @package core_course
* @copyright 2017 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_bulk_activity_completion_renderer extends plugin_renderer_base {
/**
* @deprecated since Moodle 4.0
*/
public function navigation() {
throw new coding_exception(__FUNCTION__ . '() has been removed.');
}
/**
* Render the bulk completion tab.
*
* @param Array|stdClass $data the context data to pass to the template.
* @return bool|string
*/
public function bulkcompletion($data) {
return parent::render_from_template('core_course/bulkactivitycompletion', $data);
}
/**
* Render the default completion tab.
*
* @param array|stdClass $data the context data to pass to the template.
* @param array $modules The modules that have been sent through the form.
* @param moodleform $form The current form that has been sent.
* @return bool|string
*/
public function defaultcompletion($data, $modules, $form) {
$course = get_course($data->courseid);
foreach ($data->modules as $module) {
// If the user can manage this module, then the activity completion form needs to be returned too, without the
// cancel button (so only "Save changes" button is displayed).
if ($module->canmanage) {
// Only create the form if it's different from the one that has been sent.
$modform = $form;
if (empty($form) || !in_array($module->id, array_keys($modules))) {
$modform = new \core_completion_defaultedit_form(
null,
[
'course' => $course,
'modules' => [
$module->id => $module,
],
'displaycancel' => false,
'forceuniqueid' => true,
],
);
$module->modulecollapsed = true;
}
$moduleform = manager::get_module_form($module->name, $course);
if ($moduleform) {
$module->formhtml = $modform->render();
} else {
// If the module form is not available, then display a message.
$module->formhtml = $this->output->notification(
get_string('incompatibleplugin', 'completion'),
\core\output\notification::NOTIFY_INFO,
false
);
}
}
}
$data->issite = $course->id == SITEID;
return parent::render_from_template('core_course/defaultactivitycompletion', $data);
}
/**
* Renders the form for bulk editing activities completion
*
* @param moodleform $form
* @param array $activities
* @return string
*/
public function edit_bulk_completion($form, $activities) {
ob_start();
$form->display();
$formhtml = ob_get_contents();
ob_end_clean();
$data = (object)[
'form' => $formhtml,
'activities' => array_values($activities),
'activitiescount' => count($activities),
];
return parent::render_from_template('core_course/editbulkactivitycompletion', $data);
}
/**
* Renders the form for editing default completion
*
* @param moodleform $form
* @param array $modules
* @return string
* @deprecated since Moodle 4.3 MDL-78528
* @todo MDL-78711 This will be deleted in Moodle 4.7
*/
public function edit_default_completion($form, $modules) {
debugging('edit_default_completion() is deprecated and will be removed.', DEBUG_DEVELOPER);
ob_start();
$form->display();
$formhtml = ob_get_contents();
ob_end_clean();
$data = (object)[
'form' => $formhtml,
'modules' => array_values($modules),
'modulescount' => count($modules),
];
return parent::render_from_template('core_course/editdefaultcompletion', $data);
}
/**
* Renders the course completion action bar.
*
* @param \core_course\output\completion_action_bar $actionbar
* @return string The HTML output
*/
public function render_course_completion_action_bar(\core_course\output\completion_action_bar $actionbar): string {
$data = $actionbar->export_for_template($this->output);
return $this->output->render_from_template('core_course/completion_action_bar', $data);
}
}
@@ -0,0 +1,171 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output;
use context_coursecat;
use core_course_category;
use course_request;
use moodle_page;
use moodle_url;
/**
* Class responsible for generating the action bar (tertiary nav) elements in an individual category page
*
* @package core
* @copyright 2021 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_action_bar extends manage_categories_action_bar {
/**
* @var object The current category we are referring to.
*/
protected $category;
/**
* Constructor category_action_bar
*
* @param moodle_page $page The page object
* @param object $category
* @param object|null $course The course that we are generating the nav for
* @param string|null $searchvalue
*/
public function __construct(moodle_page $page, object $category, ?object $course = null, ?string $searchvalue = null) {
$this->category = $category;
parent::__construct($page, 'courses', $course, $searchvalue);
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_category_select(\renderer_base $output): ?object {
if (!$this->searchvalue && !core_course_category::is_simple_site()) {
$categories = core_course_category::make_categories_list();
if (count($categories) > 1) {
foreach ($categories as $id => $cat) {
$url = new moodle_url($this->page->url, ['categoryid' => $id]);
$options[$url->out()] = $cat;
}
$currenturl = new moodle_url($this->page->url, ['categoryid' => $this->category->id]);
$select = new \url_select($options, $currenturl, null);
$select->set_label(get_string('categories'), ['class' => 'sr-only']);
$select->class .= ' text-truncate w-100';
return $select->export_for_template($output);
}
}
return null;
}
/**
* Gets the additional options to be displayed within a 'More' dropdown in the tertiary navigation.
* The predefined order defined by UX is:
* - Add a course
* - Add a sub cat
* - Manage course
* - Request a course
* - Course pending approval
*
* @return array
*/
protected function get_additional_category_options(): array {
global $CFG, $DB;
if ($this->category->is_uservisible()) {
$context = get_category_or_system_context($this->category->id);
if (has_capability('moodle/course:create', $context)) {
$params = [
'category' => $this->category->id ?: $CFG->defaultrequestcategory,
'returnto' => $this->category->id ? 'category' : 'topcat'
];
$options[0] = [
'url' => new moodle_url('/course/edit.php', $params),
'string' => get_string('addnewcourse')
];
}
if (!empty($CFG->enablecourserequests)) {
// Display an option to request a new course.
if (course_request::can_request($context)) {
$params = [];
if ($context instanceof context_coursecat) {
$params['category'] = $context->instanceid;
}
$options[3] = [
'url' => new moodle_url('/course/request.php', $params),
'string' => get_string('requestcourse')
];
}
// Display the manage pending requests option.
if (has_capability('moodle/site:approvecourse', $context)) {
$disabled = !$DB->record_exists('course_request', array());
if (!$disabled) {
$options[4] = [
'url' => new moodle_url('/course/pending.php'),
'string' => get_string('coursespending')
];
}
}
}
}
if ($this->category->can_create_course() || $this->category->has_manage_capability()) {
// Add 'Manage' button if user has permissions to edit this category.
$options[2] = [
'url' => new moodle_url('/course/management.php', ['categoryid' => $this->category->id]),
'string' => get_string('managecourses')
];
if ($this->category->has_manage_capability()) {
$addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $this->category->id));
$options[1] = [
'url' => $addsubcaturl,
'string' => get_string('addsubcategory')
];
}
}
// We have stored the options in a predefined order. Sort it based on index and return.
if (isset($options)) {
sort($options);
return ['options' => $options];
}
return [];
}
/**
* Export the content to be displayed on the category page.
*
* @param \renderer_base $output
* @return array Consists of the following:
* - categoryselect A list of available categories to be fed into a urlselect
* - search The course search form
* - additionaloptions Additional actions that can be performed in a category
*/
public function export_for_template(\renderer_base $output): array {
return [
'categoryselect' => $this->get_category_select($output),
'search' => $this->get_search_form(),
'additionaloptions' => $this->get_additional_category_options()
];
}
}
@@ -0,0 +1,75 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output;
use core\output\select_menu;
use core_completion\manager;
use moodle_url;
use renderable;
use renderer_base;
use templatable;
use url_select;
/**
* Renderable class for the action bar elements in the course completion pages.
*
* @package core_course
* @copyright 2022 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_action_bar implements templatable, renderable {
/** @var int $courseid The course id. */
private $courseid;
/** @var moodle_url $currenturl The URL of the current page. */
private $currenturl;
/**
* The class constructor.
*
* @param int $courseid The course id.
* @param moodle_url $pageurl The URL of the current page.
*/
public function __construct(int $courseid, moodle_url $pageurl) {
$this->courseid = $courseid;
$this->currenturl = $pageurl;
}
/**
* Export the data for the mustache template.
*
* @param renderer_base $output renderer to be used to render the action bar elements.
* @return array The array which contains the data required to output the tertiary navigation selector for the course
* completion pages.
*/
public function export_for_template(renderer_base $output): array {
$selectmenu = new select_menu(
'coursecompletionnavigation',
manager::get_available_completion_options($this->courseid),
$this->currenturl->out(false)
);
$selectmenu->set_label(
get_string('coursecompletionnavigation', 'completion'),
['class' => 'sr-only']
);
return [
'navigation' => $selectmenu->export_for_template($output),
];
}
}
@@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Prepares content for buttons/links to course content export/download.
*
* @package core_course
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
/**
* Prepares content for buttons/links to course content export/download.
*
* @package core_course
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_export_link {
/**
* Prepare and return the various attributes required for a link/button to populate/trigger the download course content modal.
*
* @param \context $context The context of the content being exported.
* @return \stdClass
*/
public static function get_attributes(\context $context): \stdClass {
global $CFG;
$downloadattr = new \stdClass();
$downloadattr->url = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id]);
$downloadattr->displaystring = get_string('downloadcoursecontent', 'course');
$maxfilesize = display_size($CFG->maxsizeperdownloadcoursefile);
$downloadlink = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id, 'download' => 1]);
$downloadattr->elementattributes = [
'data-downloadcourse' => 1,
'data-download-body' => get_string('downloadcourseconfirmation', 'course', $maxfilesize),
'data-download-button-text' => get_string('download'),
'data-download-link' => $downloadlink->out(false),
'data-download-title' => get_string('downloadcoursecontent', 'course'),
'data-overrides-tree-activation-key-handler' => 1,
];
return $downloadattr;
}
}
@@ -0,0 +1,164 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output;
use moodle_page;
use moodle_url;
/**
* Class responsible for generating the action bar (tertiary nav) elements in the category management page
*
* @package core
* @copyright 2021 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manage_categories_action_bar implements \renderable {
/** @var object $course The course we are dealing with. */
protected $course;
/** @var moodle_page $page The current page. */
protected $page;
/** @var string|null $viewmode The viewmode of the underlying page - Course and categories, categories or courses */
protected $viewmode;
/** @var string|null $heading The heading to display */
protected $heading;
/** @var string|null $searchvalue The search value if any */
protected $searchvalue;
/**
* Constructor for the manage_categories_action_bar
*
* @param moodle_page $page The page object
* @param string $viewmode The type of page we are viewing.
* @param object|null $course The course that we are generating the nav for
* @param string|null $searchvalue The search value if applicable
*/
public function __construct(moodle_page $page, string $viewmode, ?object $course, ?string $searchvalue) {
$this->course = $course;
$this->page = $page;
$this->viewmode = $viewmode;
$this->searchvalue = $searchvalue;
if ($searchvalue) {
$this->heading = get_string('searchresults');
}
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_dropdown(\renderer_base $output): ?object {
// If a search is being performed then no need to display the dropdown.
if ($this->searchvalue) {
return null;
}
$modes = \core_course\management\helper::get_management_viewmodes();
$activeurl = null;
$content = [];
foreach ($modes as $mode => $description) {
$url = new moodle_url($this->page->url, ['view' => $mode]);
$content[$url->out()] = $description;
if ($this->viewmode == $mode) {
$activeurl = $url->out();
$this->heading = get_string("manage$mode");
}
}
// Default to the first option if asking for default. This is combined.
if (!$activeurl && $this->viewmode === 'default') {
$activeurl = array_key_first($content);
$this->heading = get_string("managecombined");
}
if ($content) {
$urlselect = new \url_select($content, $activeurl, null);
$urlselect->set_label(get_string('viewing'), ['class' => 'sr-only']);
return $urlselect->export_for_template($output);
}
return null;
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_category_select(\renderer_base $output): ?object {
if (!$this->searchvalue && $this->viewmode === 'courses') {
$categories = \core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
if (!$categories) {
return null;
}
$currentcat = $this->page->url->param('categoryid');
foreach ($categories as $id => $cat) {
$url = new moodle_url($this->page->url, ['categoryid' => $id]);
if ($id == $currentcat) {
$currenturl = $url->out();
}
$options[$url->out()] = $cat;
}
$select = new \url_select($options, $currenturl);
$select->set_label(get_string('category'), ['class' => 'sr-only']);
$select->class .= ' text-truncate w-100';
return $select->export_for_template($output);
}
return null;
}
/**
* Get the search box
*
* @return array
*/
protected function get_search_form(): array {
$searchform = [
'btnclass' => 'btn-primary',
'inputname' => 'search',
'searchstring' => get_string('searchcourses'),
'query' => $this->searchvalue
];
if (\core_course_category::has_capability_on_any(['moodle/category:manage', 'moodle/course:create'])) {
$searchform['action'] = new moodle_url('/course/management.php');
} else {
$searchform['action'] = new moodle_url('/course/search.php');
}
return $searchform;
}
/**
* Export the content to be displayed on the participants page.
*
* @param \renderer_base $output
* @return array Consists of the following:
* - urlselect A stdclass representing the standard navigation options to be fed into a urlselect
* - renderedcontent Rendered content to be displayed in line with the tertiary nav
*/
public function export_for_template(\renderer_base $output): array {
return [
'urlselect' => $this->get_dropdown($output),
'categoryselect' => $this->get_category_select($output),
'search' => $this->get_search_form(),
'heading' => $this->heading,
];
}
}
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains activity_list renderable used for the recommended activities page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output\recommendations;
/**
* Activity list renderable.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_list implements \renderable, \templatable {
/** @var array $modules activities to display in the recommendations page. */
protected $modules;
/** @var string $searchquery The search query. */
protected $searchquery;
/**
* Constructor method.
*
* @param array $modules Activities to display
* @param string $searchquery The search query if present
*/
public function __construct(array $modules, string $searchquery) {
$this->modules = $modules;
$this->searchquery = $searchquery;
}
/**
* Export method to configure information into something the template can use.
*
* @param \renderer_base $output Not actually used.
* @return array Template context information.
*/
public function export_for_template(\renderer_base $output): array {
$info = array_map(function($module) {
return [
'id' => $module->id ?? '',
'name' => $module->title,
'componentname' => $module->componentname,
'icon' => $module->icon,
'recommended' => $module->recommended ?? ''
];
}, $this->modules);
return [
'categories' => [
[
'categoryname' => get_string('activities'),
'hascategorydata' => !empty($info),
'categorydata' => $info
]
],
'search' => [
'query' => $this->searchquery,
'searchresultsnumber' => count($this->modules)
]
];
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains renderers for the recommendations page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output\recommendations;
/**
* Main renderer for the recommendations page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Render a list of activities to recommend.
*
* @param \core_course\output\recommendations\activity_list $page activity list renderable
* @return string html for displaying.
*/
public function render_activity_list(\core_course\output\recommendations\activity_list $page): string {
$data = $page->export_for_template($this);
return parent::render_from_template('core_course/activity_list', $data);
}
}

Some files were not shown because too many files have changed in this diff Show More