From 81b0d0533fd22c5ac1e82dbd3ed0a997e01a9d33 Mon Sep 17 00:00:00 2001 From: Sujith Haridasan Date: Mon, 8 Nov 2021 16:53:39 +0530 Subject: [PATCH 01/57] MDL-72470 calendar: Add breadcrumbs to calendar page Add breadcrumbs to the calendar page. --- calendar/export.php | 6 ++++-- calendar/import.php | 7 ++++--- calendar/managesubscriptions.php | 7 ++++--- calendar/view.php | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/calendar/export.php b/calendar/export.php index 59e785b18ab5d..cb48f4cc8b771 100644 --- a/calendar/export.php +++ b/calendar/export.php @@ -123,11 +123,13 @@ $pagetitle = get_string('export', 'calendar'); $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $managesubscriptionsurl); -$PAGE->navbar->add($pagetitle); +$PAGE->navbar->add(get_string('exportcalendar', 'calendar'), $url); // Print title and header. +$headingstr = get_string('calendar', 'core_calendar'); +$headingstr = ($courseid != SITEID && !empty($courseid)) ? "{$headingstr}: {$COURSE->shortname}" : $headingstr; $PAGE->set_title($course->shortname.': '.get_string('calendar', 'calendar').': '.$pagetitle); -$PAGE->set_heading($course->fullname); +$PAGE->set_heading($headingstr); $PAGE->set_pagelayout('standard'); $renderer = $PAGE->get_renderer('core_calendar'); diff --git a/calendar/import.php b/calendar/import.php index 1e561b7009f66..52bb68eeb0677 100644 --- a/calendar/import.php +++ b/calendar/import.php @@ -98,14 +98,15 @@ } $heading = get_string('importcalendar', 'calendar'); +$headingstr = get_string('calendar', 'core_calendar'); +$headingstr = ($courseid != SITEID && !empty($courseid)) ? "{$headingstr}: {$COURSE->shortname}" : $headingstr; $pagetitle = $course->shortname . ': ' . get_string('calendar', 'calendar') . ': ' . $heading; $PAGE->set_title($pagetitle); -$PAGE->set_heading($heading); +$PAGE->set_heading($headingstr); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('admin'); -$PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $managesubscriptionsurl); -$PAGE->navbar->add($heading); +$PAGE->navbar->add($heading, $pageurl); $renderer = $PAGE->get_renderer('core_calendar'); $customdata = [ diff --git a/calendar/managesubscriptions.php b/calendar/managesubscriptions.php index 316b1f6b3952c..26c03a170fac3 100644 --- a/calendar/managesubscriptions.php +++ b/calendar/managesubscriptions.php @@ -67,8 +67,7 @@ if (!calendar_user_can_add_event($course)) { print_error('errorcannotimport', 'calendar'); } - -$PAGE->navbar->add(get_string('managesubscriptions', 'calendar')); +$PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $PAGE->url); $types = calendar_get_allowed_event_types($courseid); @@ -140,7 +139,9 @@ // Print title and header. $PAGE->set_title("$course->shortname: ".get_string('calendar', 'calendar').": ".get_string('subscriptions', 'calendar')); -$PAGE->set_heading($course->fullname); +$heading = get_string('calendar', 'core_calendar'); +$heading = ($courseid != SITEID && !empty($courseid)) ? "{$heading}: {$COURSE->shortname}" : $heading; +$PAGE->set_heading($heading); $renderer = $PAGE->get_renderer('core_calendar'); diff --git a/calendar/view.php b/calendar/view.php index 827bbdaa2338f..1f3f8b8409360 100644 --- a/calendar/view.php +++ b/calendar/view.php @@ -138,8 +138,8 @@ $PAGE->set_pagelayout('standard'); $PAGE->set_title("$course->shortname: $strcalendar: $pagetitle"); -$headingstr = ($iscoursecalendar) ? get_string('coursecalendar', 'core_calendar', $COURSE->shortname) : - get_string('calendar', 'core_calendar'); +$headingstr = get_string('calendar', 'core_calendar'); +$headingstr = ($iscoursecalendar) ? "{$headingstr}: {$COURSE->shortname}" : $headingstr; $PAGE->set_heading($headingstr); $renderer = $PAGE->get_renderer('core_calendar'); From 697594cd957e37814534d146e6ce7a8b553d602e Mon Sep 17 00:00:00 2001 From: David Matamoros Date: Fri, 5 Nov 2021 15:42:05 +0100 Subject: [PATCH 02/57] MDL-73011 core: Clean previous tab content when switching tabs Co-authored-by: Paul Holden --- lib/amd/build/dynamic_tabs.min.js | 2 +- lib/amd/build/dynamic_tabs.min.js.map | 2 +- lib/amd/src/dynamic_tabs.js | 42 ++++++++++++--------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/amd/build/dynamic_tabs.min.js b/lib/amd/build/dynamic_tabs.min.js index 51bdd53451922..c5aa179aecb62 100644 --- a/lib/amd/build/dynamic_tabs.min.js +++ b/lib/amd/build/dynamic_tabs.min.js @@ -1,2 +1,2 @@ -define ("core/dynamic_tabs",["exports","jquery","core/templates","core/notification","core/pending","core/str","core/local/repository/dynamic_tabs","core_form/changechecker"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=i(b);c=i(c);d=i(d);e=i(e);function i(a){return a&&a.__esModule?a:{default:a}}function j(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function k(a){for(var b=1,c;ba.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Dynamic Tabs UI element with AJAX loading of tabs content\n *\n * @module core/dynamic_tabs\n * @copyright 2021 David Matamoros based on code from Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_strings as getStrings} from 'core/str';\nimport {getContent} from 'core/local/repository/dynamic_tabs';\nimport {isAnyWatchedFormDirty, resetAllFormDirtyStates} from 'core_form/changechecker';\n\nconst SELECTORS = {\n dynamicTabs: '.dynamictabs',\n activeTab: '.dynamictabs .nav-link.active',\n allActiveTabs: '.dynamictabs .nav-link[data-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n const tabToggle = $(SELECTORS.tabToggle);\n\n // Listen to click, warn user if they are navigating away with unsaved form changes.\n tabToggle.on('click', (event) => {\n if (!isAnyWatchedFormDirty()) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n getStrings([\n {key: 'changesmade', component: 'moodle'},\n {key: 'changesmadereallygoaway', component: 'moodle'},\n {key: 'confirm', component: 'moodle'},\n ]).then(([strChangesMade, strChangesMadeReally, strConfirm]) =>\n // Reset form dirty state on confirmation, re-trigger the event.\n Notification.confirm(strChangesMade, strChangesMadeReally, strConfirm, null, () => {\n resetAllFormDirtyStates();\n $(event.target).trigger(event.type);\n })\n ).catch(Notification.exception);\n });\n\n // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n tabToggle.on('shown.bs.tab', function() {\n const tab = $($(this).attr('href'));\n if (tab.length !== 1) {\n return;\n }\n loadTab(tab.attr('id'));\n });\n\n if (!openTabFromHash()) {\n const tabs = document.querySelector(SELECTORS.allActiveTabs);\n if (tabs) {\n openTab(tabs.getAttribute('aria-controls'));\n } else {\n // We may hide tabs if there is only one available, just load the contents of the first tab.\n const tabPane = document.querySelector(SELECTORS.tabPane);\n if (tabPane) {\n tabPane.classList.add('active', 'show');\n loadTab(tabPane.getAttribute('id'));\n }\n }\n }\n};\n\n/**\n * Show \"loading\" template instead of a node\n *\n * @param {HTMLElement} node\n * @return {Promise}\n */\nconst indicateNodeIsLoading = (node) => {\n return Templates.render('core/loading', {})\n .then((html, js) => {\n return Templates.replaceNodeContents(node, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Returns id/name of the currently active tab\n *\n * @return {String|null}\n */\nconst getActiveTabName = () => {\n const element = document.querySelector(SELECTORS.activeTab);\n return element?.getAttribute('aria-controls') || null;\n};\n\n/**\n * Returns the id/name of the first tab\n *\n * @return {String|null}\n */\nconst getFirstTabName = () => {\n const element = document.querySelector(SELECTORS.tabContent);\n return element?.dataset.tabContent || null;\n};\n\n/**\n * Loads contents of a tab using an AJAX request\n *\n * @param {String} tabName\n * @param {Object} additionalData additional data to pass to WS\n */\nconst loadTab = (tabName, additionalData = {}) => {\n // If tabName is not specified find the active tab, or if is not defined, the first available tab.\n tabName = tabName ?? getActiveTabName() ?? getFirstTabName();\n\n const tab = document.querySelector(SELECTORS.forTabName(tabName));\n if (!tab) {\n return;\n }\n\n const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName);\n const tabdata = tab.closest(SELECTORS.dynamicTabs);\n const wsData = {\n 'reportid': tabdata.dataset.reportid,\n 'id': tabdata.dataset.id,\n ...additionalData\n };\n let tabjs = '';\n tab.textContent = '';\n\n indicateNodeIsLoading(tab)\n .then(() => {\n return getContent(tab.dataset.tabClass, JSON.stringify(wsData));\n })\n .then((data) => {\n tabjs = data.javascript;\n return Templates.render(data.template, JSON.parse(data.content));\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(tab, html, js + tabjs);\n })\n .then(() => {\n pendingPromise.resolve();\n return null;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Return the tab given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTab = (tabName) => {\n return document.querySelector(SELECTORS.forTabId(tabName));\n};\n\n/**\n * Return the tab pane given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTabPane = (tabName) => {\n return document.getElementById(tabName);\n};\n\n/**\n * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves\n *\n * @param {String} tabName\n * @return {Boolean}\n */\nconst openTab = (tabName) => {\n const tab = getTab(tabName);\n if (!tab) {\n return false;\n }\n\n loadTab(tabName);\n tab.classList.add('active');\n getTabPane(tabName).classList.add('active', 'show');\n return true;\n};\n\n/**\n * If there is a location hash that is the same as the tab name - open this tab.\n *\n * @return {Boolean}\n */\nconst openTabFromHash = () => {\n const hash = document.location.hash;\n if (hash.match(/^#\\w+$/g)) {\n return openTab(hash.replace(/^#/g, ''));\n }\n\n return false;\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/dynamic_tabs.js"],"names":["SELECTORS","dynamicTabs","activeTab","allActiveTabs","tabContent","tabToggle","tabPane","forTabName","tabName","forTabId","init","on","event","preventDefault","stopPropagation","key","component","then","strChangesMade","strChangesMadeReally","strConfirm","Notification","confirm","target","trigger","type","catch","exception","previousTabName","getActiveTabName","previousTab","document","querySelector","textContent","tab","attr","length","loadTab","openTabFromHash","tabs","openTab","getAttribute","classList","add","element","getFirstTabName","dataset","additionalData","pendingPromise","Pending","tabdata","closest","wsData","reportid","id","tabjs","tabClass","JSON","stringify","data","javascript","Templates","render","template","parse","content","html","js","replaceNodeContents","resolve","getTab","getTabPane","getElementById","hash","location","match","replace"],"mappings":"0SAuBA,OACA,OAEA,OACA,O,6tDAKMA,CAAAA,CAAS,CAAG,CACdC,WAAW,CAAE,cADC,CAEdC,SAAS,CAAE,+BAFG,CAGdC,aAAa,CAAE,4DAHD,CAIdC,UAAU,CAAE,2CAJE,CAKdC,SAAS,CAAE,wBALG,CAMdC,OAAO,CAAE,wBANK,CASRC,UATQ,CASK,SAAAC,CAAO,mDAAuCA,CAAvC,QATZ,CAURC,QAVQ,CAUG,SAAAD,CAAO,6DAA+CA,CAA/C,QAVV,C,CAeLE,CAAI,CAAG,UAAM,CACtB,GAAML,CAAAA,CAAS,CAAG,cAAEL,CAAS,CAACK,SAAZ,CAAlB,CAGAA,CAAS,CAACM,EAAV,CAAa,OAAb,CAAsB,SAACC,CAAD,CAAW,CAC7B,GAAI,CAAC,6BAAL,CAA8B,CAC1B,MACH,CAEDA,CAAK,CAACC,cAAN,GACAD,CAAK,CAACE,eAAN,GAEA,kBAAW,CACP,CAACC,GAAG,CAAE,aAAN,CAAqBC,SAAS,CAAE,QAAhC,CADO,CAEP,CAACD,GAAG,CAAE,yBAAN,CAAiCC,SAAS,CAAE,QAA5C,CAFO,CAGP,CAACD,GAAG,CAAE,SAAN,CAAiBC,SAAS,CAAE,QAA5B,CAHO,CAAX,EAIGC,IAJH,CAIQ,yBAAEC,CAAF,MAAkBC,CAAlB,MAAwCC,CAAxC,YAEJC,WAAaC,OAAb,CAAqBJ,CAArB,CAAqCC,CAArC,CAA2DC,CAA3D,CAAuE,IAAvE,CAA6E,UAAM,CAC/E,gCACA,cAAER,CAAK,CAACW,MAAR,EAAgBC,OAAhB,CAAwBZ,CAAK,CAACa,IAA9B,CACH,CAHD,CAFI,CAJR,EAUEC,KAVF,CAUQL,UAAaM,SAVrB,CAWH,CAnBD,EAuBAtB,CAAS,CACJM,EADL,CACQ,aADR,CACuB,UAAW,CAE1B,GAAMiB,CAAAA,CAAe,CAAGC,CAAgB,EAAxC,CACA,GAAID,CAAJ,CAAqB,CACjB,GAAME,CAAAA,CAAW,CAAGC,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACO,UAAV,CAAqBqB,CAArB,CAAvB,CAApB,CACAE,CAAW,CAACG,WAAZ,CAA0B,EAC7B,CACJ,CARL,EASKtB,EATL,CASQ,cATR,CASwB,UAAW,CAC3B,GAAMuB,CAAAA,CAAG,CAAG,cAAE,cAAE,IAAF,EAAQC,IAAR,CAAa,MAAb,CAAF,CAAZ,CACA,GAAmB,CAAf,GAAAD,CAAG,CAACE,MAAR,CAAsB,CAClB,MACH,CACDC,CAAO,CAACH,CAAG,CAACC,IAAJ,CAAS,IAAT,CAAD,CACV,CAfL,EAiBA,GAAI,CAACG,CAAe,EAApB,CAAwB,CACpB,GAAMC,CAAAA,CAAI,CAAGR,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACG,aAAjC,CAAb,CACA,GAAIoC,CAAJ,CAAU,CACNC,CAAO,CAACD,CAAI,CAACE,YAAL,CAAkB,eAAlB,CAAD,CACV,CAFD,IAEO,CAEH,GAAMnC,CAAAA,CAAO,CAAGyB,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACM,OAAjC,CAAhB,CACA,GAAIA,CAAJ,CAAa,CACTA,CAAO,CAACoC,SAAR,CAAkBC,GAAlB,CAAsB,QAAtB,CAAgC,MAAhC,EACAN,CAAO,CAAC/B,CAAO,CAACmC,YAAR,CAAqB,IAArB,CAAD,CACV,CACJ,CACJ,CACJ,C,aAOKZ,CAAAA,CAAgB,CAAG,UAAM,CAC3B,GAAMe,CAAAA,CAAO,CAAGb,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACE,SAAjC,CAAhB,CACA,MAAO,QAAA0C,CAAO,WAAPA,SAAAA,CAAO,CAAEH,YAAT,CAAsB,eAAtB,IAA0C,IACpD,C,CAOKI,CAAe,CAAG,UAAM,CAC1B,GAAMD,CAAAA,CAAO,CAAGb,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACI,UAAjC,CAAhB,CACA,MAAO,QAAAwC,CAAO,WAAPA,SAAAA,CAAO,CAAEE,OAAT,CAAiB1C,UAAjB,GAA+B,IACzC,C,CAQKiC,CAAO,CAAG,SAAC7B,CAAD,CAAkC,SAAxBuC,CAAwB,wDAAP,EAAO,CAE9CvC,CAAO,qBAAGA,CAAH,gBAAcqB,CAAgB,EAA9B,gBAAoCgB,CAAe,EAA1D,CAEA,GAAMX,CAAAA,CAAG,CAAGH,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACO,UAAV,CAAqBC,CAArB,CAAvB,CAAZ,CACA,GAAI,CAAC0B,CAAL,CAAU,CACN,MACH,CAP6C,GASxCc,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,6BAA+BzC,CAA3C,CATuB,CAUxC0C,CAAO,CAAGhB,CAAG,CAACiB,OAAJ,CAAYnD,CAAS,CAACC,WAAtB,CAV8B,CAWxCmD,CAAM,IACR,SAAYF,CAAO,CAACJ,OAAR,CAAgBO,QADpB,CAER,GAAMH,CAAO,CAACJ,OAAR,CAAgBQ,EAFd,EAGLP,CAHK,CAXkC,CAgB1CQ,CAAK,CAAG,EAhBkC,CAkB9C,yBAAmBrB,CAAnB,EACCjB,IADD,CACM,UAAM,CACR,MAAO,iBAAWiB,CAAG,CAACY,OAAJ,CAAYU,QAAvB,CAAiCC,IAAI,CAACC,SAAL,CAAeN,CAAf,CAAjC,CACV,CAHD,EAICnC,IAJD,CAIM,SAAC0C,CAAD,CAAU,CACZJ,CAAK,CAAGI,CAAI,CAACC,UAAb,CACA,MAAOC,WAAUC,MAAV,CAAiBH,CAAI,CAACI,QAAtB,CAAgCN,IAAI,CAACO,KAAL,CAAWL,CAAI,CAACM,OAAhB,CAAhC,CACV,CAPD,EAQChD,IARD,CAQM,SAACiD,CAAD,CAAOC,CAAP,CAAc,CAChB,MAAON,WAAUO,mBAAV,CAA8BlC,CAA9B,CAAmCgC,CAAnC,CAAyCC,CAAE,CAAGZ,CAA9C,CACV,CAVD,EAWCtC,IAXD,CAWM,UAAM,CACR+B,CAAc,CAACqB,OAAf,GACA,MAAO,KACV,CAdD,EAeC3C,KAfD,CAeOL,UAAaM,SAfpB,CAgBH,C,CAQK2C,CAAM,CAAG,SAAC9D,CAAD,CAAa,CACxB,MAAOuB,CAAAA,QAAQ,CAACC,aAAT,CAAuBhC,CAAS,CAACS,QAAV,CAAmBD,CAAnB,CAAvB,CACV,C,CAQK+D,CAAU,CAAG,SAAC/D,CAAD,CAAa,CAC5B,MAAOuB,CAAAA,QAAQ,CAACyC,cAAT,CAAwBhE,CAAxB,CACV,C,CAQKgC,CAAO,CAAG,SAAChC,CAAD,CAAa,CACzB,GAAM0B,CAAAA,CAAG,CAAGoC,CAAM,CAAC9D,CAAD,CAAlB,CACA,GAAI,CAAC0B,CAAL,CAAU,CACN,QACH,CAEDG,CAAO,CAAC7B,CAAD,CAAP,CACA0B,CAAG,CAACQ,SAAJ,CAAcC,GAAd,CAAkB,QAAlB,EACA4B,CAAU,CAAC/D,CAAD,CAAV,CAAoBkC,SAApB,CAA8BC,GAA9B,CAAkC,QAAlC,CAA4C,MAA5C,EACA,QACH,C,CAOKL,CAAe,CAAG,UAAM,CAC1B,GAAMmC,CAAAA,CAAI,CAAG1C,QAAQ,CAAC2C,QAAT,CAAkBD,IAA/B,CACA,GAAIA,CAAI,CAACE,KAAL,CAAW,SAAX,CAAJ,CAA2B,CACvB,MAAOnC,CAAAA,CAAO,CAACiC,CAAI,CAACG,OAAL,CAAa,KAAb,CAAoB,EAApB,CAAD,CACjB,CAED,QACH,C","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 .\n\n/**\n * Dynamic Tabs UI element with AJAX loading of tabs content\n *\n * @module core/dynamic_tabs\n * @copyright 2021 David Matamoros based on code from Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_strings as getStrings} from 'core/str';\nimport {getContent} from 'core/local/repository/dynamic_tabs';\nimport {isAnyWatchedFormDirty, resetAllFormDirtyStates} from 'core_form/changechecker';\n\nconst SELECTORS = {\n dynamicTabs: '.dynamictabs',\n activeTab: '.dynamictabs .nav-link.active',\n allActiveTabs: '.dynamictabs .nav-link[data-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n const tabToggle = $(SELECTORS.tabToggle);\n\n // Listen to click, warn user if they are navigating away with unsaved form changes.\n tabToggle.on('click', (event) => {\n if (!isAnyWatchedFormDirty()) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n getStrings([\n {key: 'changesmade', component: 'moodle'},\n {key: 'changesmadereallygoaway', component: 'moodle'},\n {key: 'confirm', component: 'moodle'},\n ]).then(([strChangesMade, strChangesMadeReally, strConfirm]) =>\n // Reset form dirty state on confirmation, re-trigger the event.\n Notification.confirm(strChangesMade, strChangesMadeReally, strConfirm, null, () => {\n resetAllFormDirtyStates();\n $(event.target).trigger(event.type);\n })\n ).catch(Notification.exception);\n });\n\n // This code listens to Bootstrap events 'show.bs.tab' and 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n tabToggle\n .on('show.bs.tab', function() {\n // Clean content from previous tab.\n const previousTabName = getActiveTabName();\n if (previousTabName) {\n const previousTab = document.querySelector(SELECTORS.forTabName(previousTabName));\n previousTab.textContent = '';\n }\n })\n .on('shown.bs.tab', function() {\n const tab = $($(this).attr('href'));\n if (tab.length !== 1) {\n return;\n }\n loadTab(tab.attr('id'));\n });\n\n if (!openTabFromHash()) {\n const tabs = document.querySelector(SELECTORS.allActiveTabs);\n if (tabs) {\n openTab(tabs.getAttribute('aria-controls'));\n } else {\n // We may hide tabs if there is only one available, just load the contents of the first tab.\n const tabPane = document.querySelector(SELECTORS.tabPane);\n if (tabPane) {\n tabPane.classList.add('active', 'show');\n loadTab(tabPane.getAttribute('id'));\n }\n }\n }\n};\n\n/**\n * Returns id/name of the currently active tab\n *\n * @return {String|null}\n */\nconst getActiveTabName = () => {\n const element = document.querySelector(SELECTORS.activeTab);\n return element?.getAttribute('aria-controls') || null;\n};\n\n/**\n * Returns the id/name of the first tab\n *\n * @return {String|null}\n */\nconst getFirstTabName = () => {\n const element = document.querySelector(SELECTORS.tabContent);\n return element?.dataset.tabContent || null;\n};\n\n/**\n * Loads contents of a tab using an AJAX request\n *\n * @param {String} tabName\n * @param {Object} additionalData additional data to pass to WS\n */\nconst loadTab = (tabName, additionalData = {}) => {\n // If tabName is not specified find the active tab, or if is not defined, the first available tab.\n tabName = tabName ?? getActiveTabName() ?? getFirstTabName();\n\n const tab = document.querySelector(SELECTORS.forTabName(tabName));\n if (!tab) {\n return;\n }\n\n const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName);\n const tabdata = tab.closest(SELECTORS.dynamicTabs);\n const wsData = {\n 'reportid': tabdata.dataset.reportid,\n 'id': tabdata.dataset.id,\n ...additionalData\n };\n let tabjs = '';\n\n addIconToContainer(tab)\n .then(() => {\n return getContent(tab.dataset.tabClass, JSON.stringify(wsData));\n })\n .then((data) => {\n tabjs = data.javascript;\n return Templates.render(data.template, JSON.parse(data.content));\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(tab, html, js + tabjs);\n })\n .then(() => {\n pendingPromise.resolve();\n return null;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Return the tab given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTab = (tabName) => {\n return document.querySelector(SELECTORS.forTabId(tabName));\n};\n\n/**\n * Return the tab pane given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTabPane = (tabName) => {\n return document.getElementById(tabName);\n};\n\n/**\n * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves\n *\n * @param {String} tabName\n * @return {Boolean}\n */\nconst openTab = (tabName) => {\n const tab = getTab(tabName);\n if (!tab) {\n return false;\n }\n\n loadTab(tabName);\n tab.classList.add('active');\n getTabPane(tabName).classList.add('active', 'show');\n return true;\n};\n\n/**\n * If there is a location hash that is the same as the tab name - open this tab.\n *\n * @return {Boolean}\n */\nconst openTabFromHash = () => {\n const hash = document.location.hash;\n if (hash.match(/^#\\w+$/g)) {\n return openTab(hash.replace(/^#/g, ''));\n }\n\n return false;\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file diff --git a/lib/amd/src/dynamic_tabs.js b/lib/amd/src/dynamic_tabs.js index dfe57829e5544..9e213dec9eea0 100644 --- a/lib/amd/src/dynamic_tabs.js +++ b/lib/amd/src/dynamic_tabs.js @@ -23,6 +23,7 @@ import $ from 'jquery'; import Templates from 'core/templates'; +import {addIconToContainer} from 'core/loadingicon'; import Notification from 'core/notification'; import Pending from 'core/pending'; import {get_strings as getStrings} from 'core/str'; @@ -69,15 +70,24 @@ export const init = () => { ).catch(Notification.exception); }); - // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and + // This code listens to Bootstrap events 'show.bs.tab' and 'shown.bs.tab' which is triggered using JQuery and // can not be converted yet to native events. - tabToggle.on('shown.bs.tab', function() { - const tab = $($(this).attr('href')); - if (tab.length !== 1) { - return; - } - loadTab(tab.attr('id')); - }); + tabToggle + .on('show.bs.tab', function() { + // Clean content from previous tab. + const previousTabName = getActiveTabName(); + if (previousTabName) { + const previousTab = document.querySelector(SELECTORS.forTabName(previousTabName)); + previousTab.textContent = ''; + } + }) + .on('shown.bs.tab', function() { + const tab = $($(this).attr('href')); + if (tab.length !== 1) { + return; + } + loadTab(tab.attr('id')); + }); if (!openTabFromHash()) { const tabs = document.querySelector(SELECTORS.allActiveTabs); @@ -94,19 +104,6 @@ export const init = () => { } }; -/** - * Show "loading" template instead of a node - * - * @param {HTMLElement} node - * @return {Promise} - */ -const indicateNodeIsLoading = (node) => { - return Templates.render('core/loading', {}) - .then((html, js) => { - return Templates.replaceNodeContents(node, html, js); - }).catch(Notification.exception); -}; - /** * Returns id/name of the currently active tab * @@ -150,9 +147,8 @@ const loadTab = (tabName, additionalData = {}) => { ...additionalData }; let tabjs = ''; - tab.textContent = ''; - indicateNodeIsLoading(tab) + addIconToContainer(tab) .then(() => { return getContent(tab.dataset.tabClass, JSON.stringify(wsData)); }) From c0ceaef772afc5fcff031131f3934d3c1cdaf0b0 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 11 Nov 2021 23:54:56 +0000 Subject: [PATCH 03/57] MDL-73011 reportbuilder: remove workarounds for tab switching. --- reportbuilder/tests/behat/audience.feature | 61 ++++++++++------------ 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/reportbuilder/tests/behat/audience.feature b/reportbuilder/tests/behat/audience.feature index fb8c7ea90e9dd..f62789da82a7e 100644 --- a/reportbuilder/tests/behat/audience.feature +++ b/reportbuilder/tests/behat/audience.feature @@ -31,17 +31,16 @@ Feature: Configure access to reports based on intended audience Then I click on "Add audience 'Manually added users'" "link" And I should see "Added audience 'Manually added users'" And I set the field "Add users manually" to "User 1,User 3" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" And I should see "Audience saved" And I should see "User 1" And I should not see "User 2" And I should see "User 3" And I should not see "Add an audience to this report" And I click on the "Access" dynamic tab - And I should see "User 1" in the "[role=tabpanel].active" "css_element" - And I should not see "User 2" in the "[role=tabpanel].active" "css_element" - And I should see "User 3" in the "[role=tabpanel].active" "css_element" + And I should see "User 1" in the "reportbuilder-table" "table" + And I should not see "User 2" in the "reportbuilder-table" "table" + And I should see "User 3" in the "reportbuilder-table" "table" Scenario: Configure report audience with has system role audience type Given the following "roles" exist: @@ -55,15 +54,14 @@ Feature: Configure access to reports based on intended audience When I click on "Add audience 'Assigned system role'" "link" And I should see "Added audience 'Assigned system role'" And I set the field "Select a role" to "Test role" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" Then I should see "Audience saved" And I should see "Test role" And I should not see "Add an audience to this report" And I click on the "Access" dynamic tab - And I should not see "User 1" in the "[role=tabpanel].active" "css_element" - And I should see "User 2" in the "[role=tabpanel].active" "css_element" - And I should not see "User 3" in the "[role=tabpanel].active" "css_element" + And I should not see "User 1" in the "reportbuilder-table" "table" + And I should see "User 2" in the "reportbuilder-table" "table" + And I should not see "User 3" in the "reportbuilder-table" "table" Scenario: Configure report audience with Member of cohort audience type Given the following "cohorts" exist: @@ -77,19 +75,18 @@ Feature: Configure access to reports based on intended audience When I click on "Add audience 'Member of cohort'" "link" And I should see "Added audience 'Member of cohort'" And I set the field "Select members from cohort" to "Cohort1" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" Then I should see "Audience saved" And I should see "Cohort1" And I should not see "Add an audience to this report" And I click on the "Access" dynamic tab - And I should not see "User 1" in the "[role=tabpanel].active" "css_element" - And I should not see "User 2" in the "[role=tabpanel].active" "css_element" - And I should see "User 3" in the "[role=tabpanel].active" "css_element" + And I should not see "User 1" in the "reportbuilder-table" "table" + And I should not see "User 2" in the "reportbuilder-table" "table" + And I should see "User 3" in the "reportbuilder-table" "table" Scenario: Configure report audience with Member of cohort audience type with no cohorts available Given I am on the "My report" "reportbuilder > Editor" page logged in as "admin" - And I click on the "Audience" dynamic tab + When I click on the "Audience" dynamic tab Then "Add audience 'All users'" "link" should exist # This audience type should be disabled because there are no cohorts available. And "Add audience 'Member of cohort'" "link" should not exist @@ -98,8 +95,7 @@ Feature: Configure access to reports based on intended audience Given I am on the "My report" "reportbuilder > Editor" page logged in as "admin" And I click on the "Audience" dynamic tab And I click on "Add audience 'All users'" "link" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" When I click on "Delete audience 'All users'" "button" And I click on "Delete" "button" in the "Delete audience 'All users'" "dialogue" Then I should see "Deleted audience 'All users'" @@ -111,21 +107,20 @@ Feature: Configure access to reports based on intended audience And I should see "Nothing to display" And I click on the "Audience" dynamic tab And I should see "Add an audience to this report" - Then I click on "Add audience 'Manually added users'" "link" + And I click on "Add audience 'Manually added users'" "link" And I set the field "Add users manually" to "User 1,User 3" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" - And I press "Edit audience 'Manually added users'" + And I press "Save changes" + When I press "Edit audience 'Manually added users'" And I set the field "Add users manually" to "User 2" - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" - And I should see "Audience saved" - And I should not see "User 1" in the "[role=tabpanel].active" "css_element" - And I should see "User 2" in the "[role=tabpanel].active" "css_element" - And I should not see "User 3" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" + Then I should see "Audience saved" + And I should not see "User 1" + And I should see "User 2" + And I should not see "User 3" And I click on the "Access" dynamic tab - And I should not see "User 1" in the "[role=tabpanel].active" "css_element" - And I should see "User 2" in the "[role=tabpanel].active" "css_element" - And I should not see "User 3" in the "[role=tabpanel].active" "css_element" + And I should not see "User 1" in the "reportbuilder-table" "table" + And I should see "User 2" in the "reportbuilder-table" "table" + And I should not see "User 3" in the "reportbuilder-table" "table" Scenario: View report as a user with no edit capability and set in the report audience Given the following "core_reportbuilder > Reports" exist: @@ -156,8 +151,7 @@ Feature: Configure access to reports based on intended audience And I should see "Add an audience to this report" Then I click on "Add audience 'Manually added users'" "link" And I set the field "Add users manually" to "User 1" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" And I log out And I log in as "user1" And I navigate to "Reports > Report builder > Custom reports" in site administration @@ -201,8 +195,7 @@ Feature: Configure access to reports based on intended audience And I should see "Add an audience to this report" Then I click on "Add audience 'Manually added users'" "link" And I set the field "Add users manually" to "User 1" - # It would be better to reference the report table directly, but we can't because of MDL-73011. - And I click on "Save changes" "button" in the "[role=tabpanel].active" "css_element" + And I press "Save changes" And I log out And I log in as "user1" And I navigate to "Reports > Report builder > Custom reports" in site administration From 10baa46364d27a7ec4238d63206d79daf7784203 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 9 Nov 2021 20:25:19 +0000 Subject: [PATCH 04/57] MDL-73038 course: remove course module title double encoding. --- .../templates/local/content/cm/title.mustache | 4 ++-- course/tests/behat/activities_edit_name.feature | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/course/format/templates/local/content/cm/title.mustache b/course/format/templates/local/content/cm/title.mustache index b345a04dabea1..bcb747c4ebcbb 100644 --- a/course/format/templates/local/content/cm/title.mustache +++ b/course/format/templates/local/content/cm/title.mustache @@ -36,7 +36,7 @@ - {{instancename}} {{{altname}}} + {{{instancename}}} {{{altname}}} {{/uservisible}} @@ -44,7 +44,7 @@
- {{instancename}} {{{altname}}} + {{{instancename}}} {{{altname}}}
{{/uservisible}} diff --git a/course/tests/behat/activities_edit_name.feature b/course/tests/behat/activities_edit_name.feature index 18217384e980f..1a98500d68730 100644 --- a/course/tests/behat/activities_edit_name.feature +++ b/course/tests/behat/activities_edit_name.feature @@ -4,8 +4,7 @@ Feature: Edit activity name in-place As a teacher I need to use inplace editing - @javascript - Scenario: Edit activity name in-place + Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | @@ -21,6 +20,9 @@ Feature: Edit activity name in-place | name | Test forum name | | description | Test forum description | | idnumber | forum1 | + + @javascript + Scenario: Edit activity name in-place When I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Rename activity @@ -41,3 +43,11 @@ Feature: Edit activity name in-place And I am on "Course 1" course homepage And I should see "Good news" And I should not see "Terrible news" + + @javascript + Scenario: Edit activity name in-place ensuring correct encoding + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I set the field "Edit title" in the "Test forum name" "activity" to "Good & bad news" + Then I should not see "Test forum name" in the ".course-content" "css_element" + And I should see "Good & bad news" in the ".course-content" "css_element" From 2bd34edc2d7363954cc7e1e508bec0424482929d Mon Sep 17 00:00:00 2001 From: Mahmoud Kassaei Date: Tue, 21 Sep 2021 07:30:44 +0100 Subject: [PATCH 05/57] MDL-72612 Custom user field support: Quiz report grading --- .../grading/classes/privacy/provider.php | 9 +- mod/quiz/report/grading/db/access.php | 8 +- .../report/grading/gradingsettings_form.php | 43 ++++++--- .../report/grading/lang/en/deprecated.txt | 7 ++ .../report/grading/lang/en/quiz_grading.php | 21 ++-- mod/quiz/report/grading/report.php | 96 ++++++++++++------- .../grading/tests/behat/grading.feature | 40 +++++--- .../grading/tests/privacy_provider_test.php | 27 +++++- 8 files changed, 174 insertions(+), 77 deletions(-) create mode 100644 mod/quiz/report/grading/lang/en/deprecated.txt diff --git a/mod/quiz/report/grading/classes/privacy/provider.php b/mod/quiz/report/grading/classes/privacy/provider.php index 96594c0e44d3a..de4cd0e8864fc 100644 --- a/mod/quiz/report/grading/classes/privacy/provider.php +++ b/mod/quiz/report/grading/classes/privacy/provider.php @@ -68,10 +68,10 @@ public static function export_user_preferences(int $userid) { if ($order !== null) { switch ($order) { case 'random': - $order = get_string('randomly', 'quiz_grading'); + $order = get_string('random', 'quiz_grading'); break; case 'date': - $order = get_string('bydate', 'quiz_grading'); + $order = get_string('date'); break; case 'studentfirstname': $order = get_string('studentfirstname', 'quiz_grading'); @@ -79,9 +79,8 @@ public static function export_user_preferences(int $userid) { case 'studentlastname': $order = get_string('studentlastname', 'quiz_grading'); break; - case 'idnumber': - $order = get_string('bystudentidnumber', 'quiz_grading'); - break; + default: + $order = \core_user\fields::get_display_name($order); } writer::export_user_preference('quiz_grading', 'order', $order, get_string('privacy:preference:order', 'quiz_grading')); diff --git a/mod/quiz/report/grading/db/access.php b/mod/quiz/report/grading/db/access.php index 26658c3ca58ea..9f50665e6a268 100644 --- a/mod/quiz/report/grading/db/access.php +++ b/mod/quiz/report/grading/db/access.php @@ -33,10 +33,12 @@ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ), - 'clonepermissionsfrom' => 'mod/quiz:viewreports' + 'clonepermissionsfrom' => 'mod/quiz:viewreports' ), - // Is the user allowed to see the student's idnumber while grading? + // Is the user allowed to see the student's identity fields while grading? + // Note that the name of this capability is now out-of-date, but to preserve + // backwards compatibility, the name was not changed when the functionality was updated. 'quiz/grading:viewidnumber' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, @@ -44,6 +46,6 @@ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW ), - 'clonepermissionsfrom' => 'mod/quiz:viewreports' + 'clonepermissionsfrom' => 'mod/quiz:viewreports' ) ); diff --git a/mod/quiz/report/grading/gradingsettings_form.php b/mod/quiz/report/grading/gradingsettings_form.php index 89400e22a2a4f..f4b8a8afe24e4 100644 --- a/mod/quiz/report/grading/gradingsettings_form.php +++ b/mod/quiz/report/grading/gradingsettings_form.php @@ -39,15 +39,30 @@ class quiz_grading_settings_form extends moodleform { protected $hidden = array(); protected $counts; protected $shownames; - protected $showidnumbers; - public function __construct($hidden, $counts, $shownames, $showidnumbers) { + /** @var bool $showcustomfields whether custom field values should be shown. */ + protected $showcustomfields; + + /** @var stdClass $context the quiz context. */ + protected $context; + + /** + * quiz_grading_settings_form constructor. + * + * @param array $hidden Array of options form. + * @param stdClass $counts object that stores the number of each type of attempt. + * @param bool $shownames whether student names should be shown. + * @param bool $showcustomfields whether custom field values should be shown. + * @param stdClass $context context object. + */ + public function __construct(array $hidden, stdClass $counts, bool $shownames, bool $showcustomfields, stdClass $context) { global $CFG; $this->includeauto = !empty($hidden['includeauto']); $this->hidden = $hidden; $this->counts = $counts; $this->shownames = $shownames; - $this->showidnumbers = $showidnumbers; + $this->showcustomfields = $showcustomfields; + $this->context = $context; parent::__construct($CFG->wwwroot . '/mod/quiz/report.php'); } @@ -75,18 +90,22 @@ protected function definition() { $mform->addRule('pagesize', null, 'positiveint', null, 'client'); $mform->setType('pagesize', PARAM_INT); - $orderoptions = array( - 'random' => get_string('randomly', 'quiz_grading'), - 'date' => get_string('bydate', 'quiz_grading'), - ); + $orderoptions = [ + 'random' => get_string('random', 'quiz_grading'), + 'date' => get_string('date') + ]; if ($this->shownames) { - $orderoptions['studentfirstname'] = get_string('bystudentfirstname', 'quiz_grading'); - $orderoptions['studentlastname'] = get_string('bystudentlastname', 'quiz_grading'); + $orderoptions['studentfirstname'] = get_string('firstname'); + $orderoptions['studentlastname'] = get_string('lastname'); } - if ($this->showidnumbers) { - $orderoptions['idnumber'] = get_string('bystudentidnumber', 'quiz_grading'); + // If the current user can see custom user fields, add the custom user fields to the select menu. + if ($this->showcustomfields) { + $userfieldsapi = \core_user\fields::for_identity($this->context); + foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { + $orderoptions[s($field)] = \core_user\fields::get_display_name(s($field)); + } } - $mform->addElement('select', 'order', get_string('orderattempts', 'quiz_grading'), + $mform->addElement('select', 'order', get_string('orderattemptsby', 'quiz_grading'), $orderoptions); foreach ($this->hidden as $name => $value) { diff --git a/mod/quiz/report/grading/lang/en/deprecated.txt b/mod/quiz/report/grading/lang/en/deprecated.txt new file mode 100644 index 0000000000000..72c89427244e1 --- /dev/null +++ b/mod/quiz/report/grading/lang/en/deprecated.txt @@ -0,0 +1,7 @@ +bydate,quiz_grading +bystudentidnumber,quiz_grading +bystudentfirstname,quiz_grading +bystudentlastname,quiz_grading +gradingattemptwithidnumber,quiz_grading +orderattempts,quiz_grading +randomly,quiz_grading diff --git a/mod/quiz/report/grading/lang/en/quiz_grading.php b/mod/quiz/report/grading/lang/en/quiz_grading.php index aa1224f51bb6e..08c65530fb4e2 100644 --- a/mod/quiz/report/grading/lang/en/quiz_grading.php +++ b/mod/quiz/report/grading/lang/en/quiz_grading.php @@ -28,10 +28,6 @@ $string['attemptstograde'] = 'Attempts to grade'; $string['automaticallygraded'] = 'Automatically graded'; $string['backtothelistofquestions'] = 'Back to the list of questions'; -$string['bydate'] = 'By date'; -$string['bystudentidnumber'] = 'By student ID number'; -$string['bystudentfirstname'] = 'By student first name'; -$string['bystudentlastname'] = 'By student last name'; $string['cannotloadquestioninfo'] = 'Unable to load questiontype specific question information'; $string['cannotgradethisattempt'] = 'Cannot grade this attempt.'; $string['changeoptions'] = 'Change options'; @@ -48,11 +44,11 @@ $string['gradenextungraded'] = 'Grade next {$a} ungraded attempts'; $string['gradeungraded'] = 'Grade all {$a} ungraded attempts'; $string['grading'] = 'Manual grading'; -$string['grading:viewidnumber'] = 'See student ID numbers while grading'; +$string['grading:viewidnumber'] = 'See student identity fields while grading'; $string['grading:viewstudentnames'] = 'See student names while grading'; $string['gradingall'] = 'All {$a} attempts on this question.'; $string['gradingattempt'] = 'Attempt number {$a->attempt} for {$a->fullname}'; -$string['gradingattemptwithidnumber'] = 'Attempt number {$a->attempt} for {$a->fullname} ({$a->idnumber})'; +$string['gradingattemptwithcustomfields'] = 'Attempt number {$a->attempt} for {$a->fullname} ({$a->customfields})'; $string['gradingattemptsxtoyofz'] = 'Grading attempts {$a->from} to {$a->to} of {$a->of}'; $string['gradingnextungraded'] = 'Next {$a} ungraded attempts'; $string['gradingnotallowed'] = 'You do not have permission to manually grade responses in this quiz'; @@ -65,7 +61,7 @@ $string['noquestionsfound'] = 'No manually graded questions found'; $string['nothingfound'] = 'Nothing to display'; $string['options'] = 'Options'; -$string['orderattempts'] = 'Order attempts'; +$string['orderattemptsby'] = 'Order attempts by'; $string['pluginname'] = 'Manual grading'; $string['privacy:preference:order'] = 'What order to show the attempts that need grading.'; $string['privacy:preference:pagesize'] = 'How many attempts to show on each page of the grading interface.'; @@ -74,10 +70,19 @@ $string['questionsperpage'] = 'Questions per page'; $string['questionsthatneedgrading'] = 'Questions that need grading'; $string['questiontitle'] = 'Question {$a->number} : "{$a->name}" ({$a->openspan}{$a->gradedattempts}{$a->closespan} / {$a->totalattempts} attempts {$a->openspan}graded{$a->closespan}).'; -$string['randomly'] = 'Randomly'; +$string['random'] = 'Random'; $string['saveandnext'] = 'Save and go to next page'; $string['showstudentnames'] = 'Show student names'; $string['tograde'] = 'To grade'; $string['total'] = 'Total'; $string['unknownquestion'] = 'Unknown question'; $string['updategrade'] = 'update grades'; + +// Deprecated since Moodle 4.0. +$string['bydate'] = 'By date'; +$string['bystudentidnumber'] = 'By student ID number'; +$string['bystudentfirstname'] = 'By student first name'; +$string['bystudentlastname'] = 'By student last name'; +$string['gradingattemptwithidnumber'] = 'Attempt number {$a->attempt} for {$a->fullname} ({$a->idnumber})'; +$string['orderattempts'] = 'Order attempts'; +$string['randomly'] = 'Randomly'; diff --git a/mod/quiz/report/grading/report.php b/mod/quiz/report/grading/report.php index f58c735c43a11..2a3294aae7761 100644 --- a/mod/quiz/report/grading/report.php +++ b/mod/quiz/report/grading/report.php @@ -72,6 +72,9 @@ class quiz_grading_report extends quiz_default_report { /** @var string fragment of SQL code to restrict to the relevant users. */ protected $userssql; + /** @var array extra user fields. */ + protected $extrauserfields = []; + public function display($quiz, $cm, $course) { $this->quiz = $quiz; @@ -93,7 +96,7 @@ public function display($quiz, $cm, $course) { $page = optional_param('page', 0, PARAM_INT); $order = optional_param('order', get_user_preferences('quiz_grading_order', self::DEFAULT_ORDER), - PARAM_ALPHA); + PARAM_ALPHAEXT); // Assemble the options required to reload this page. $optparams = array('includeauto', 'page'); @@ -117,14 +120,20 @@ public function display($quiz, $cm, $course) { $this->context = context_module::instance($this->cm->id); require_capability('mod/quiz:grade', $this->context); $shownames = has_capability('quiz/grading:viewstudentnames', $this->context); - $showidnumbers = has_capability('quiz/grading:viewidnumber', $this->context); - + // Whether the current user can see custom user fields. + $showcustomfields = has_capability('quiz/grading:viewidnumber', $this->context); + $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); + $customfields = []; + foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { + $customfields[] = $field; + } // Validate order. - if (!in_array($order, array('random', 'date', 'studentfirstname', 'studentlastname', 'idnumber'))) { + $orderoptions = array_merge(['random', 'date', 'studentfirstname', 'studentlastname'], $customfields); + if (!in_array($order, $orderoptions)) { $order = self::DEFAULT_ORDER; } else if (!$shownames && ($order == 'studentfirstname' || $order == 'studentlastname')) { $order = self::DEFAULT_ORDER; - } else if (!$showidnumbers && $order == 'idnumber') { + } else if (!$showcustomfields && in_array($order, $customfields)) { $order = self::DEFAULT_ORDER; } if ($order == 'random') { @@ -183,7 +192,7 @@ public function display($quiz, $cm, $course) { } $this->display_grading_interface($slot, $questionid, $grade, - $pagesize, $page, $shownames, $showidnumbers, $order, $counts); + $pagesize, $page, $shownames, $showcustomfields, $order, $counts); return true; } @@ -230,13 +239,19 @@ protected function load_attempts_by_usage_ids($qubaids) { $params[] = quiz_attempt::FINISHED; $params[] = $this->quiz->id; - $fields = 'quiza.*, u.idnumber, '; - $userfieldsapi = \core_user\fields::for_name(); - $fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects; + $fields = 'quiza.*, '; + $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); + $userfieldssql = $userfieldsapi->get_sql('u', false, '', 'userid', false); + $fields .= $userfieldssql->selects; + foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $userfield) { + $this->extrauserfields[] = s($userfield); + } + $params = array_merge($userfieldssql->params, $params); $attemptsbyid = $DB->get_records_sql(" SELECT $fields FROM {quiz_attempts} quiza JOIN {user} u ON u.id = quiza.userid + {$userfieldssql->joins} WHERE quiza.uniqueid $asql AND quiza.state = ? AND quiza.quiz = ?", $params); @@ -391,12 +406,12 @@ protected function display_index($includeauto) { * @param int $pagesize number of questions to show per page. * @param int $page current page number. * @param bool $shownames whether student names should be shown. - * @param bool $showidnumbers wither student idnumbers should be shown. + * @param bool $showcustomfields whether custom field values should be shown. * @param string $order preferred order of attempts. * @param stdClass $counts object that stores the number of each type of attempt. */ protected function display_grading_interface($slot, $questionid, $grade, - $pagesize, $page, $shownames, $showidnumbers, $order, $counts) { + $pagesize, $page, $shownames, $showcustomfields, $order, $counts) { if ($pagesize * $page >= $counts->$grade) { $page = 0; @@ -413,7 +428,7 @@ protected function display_grading_interface($slot, $questionid, $grade, if (array_key_exists('includeauto', $this->viewoptions)) { $hidden['includeauto'] = $this->viewoptions['includeauto']; } - $mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showidnumbers); + $mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showcustomfields, $this->context); // Tell the form the current settings. $settings = new stdClass(); @@ -468,7 +483,7 @@ protected function display_grading_interface($slot, $questionid, $grade, $slot, $displayoptions, $this->questions[$slot]->number, - $this->get_question_heading($attempt, $shownames, $showidnumbers) + $this->get_question_heading($attempt, $shownames, $showcustomfields) ); } @@ -621,6 +636,13 @@ protected function get_usage_ids_where_question_in_state($summarystate, $slot, $qubaids = $this->get_qubaids_condition(); $params = []; + $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); + $userfieldssql = $userfieldsapi->get_sql('u', true, '', 'userid', true); + $params = array_merge($params, $userfieldssql->params); + $customfields = []; + foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { + $customfields[] = $field; + } if ($orderby == 'date') { list($statetest, $params) = $dm->in_summary_state_test( 'manuallygraded', false, 'mangrstate'); @@ -630,20 +652,16 @@ protected function get_usage_ids_where_question_in_state($summarystate, $slot, WHERE sortqas.questionattemptid = qa.id AND sortqas.state $statetest )"; - } else if ($orderby == 'studentfirstname' || $orderby == 'studentlastname' || $orderby == 'idnumber') { - $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id "; + } else if ($orderby == 'studentfirstname' || $orderby == 'studentlastname' || in_array($orderby, $customfields)) { + $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id {$userfieldssql->joins}"; // For name sorting, map orderby form value to // actual column names; 'idnumber' maps naturally. - switch ($orderby) { - case "studentlastname": - $orderby = "u.lastname, u.firstname"; - break; - case "studentfirstname": - $orderby = "u.firstname, u.lastname"; - break; - case "idnumber": - $orderby = "u.idnumber"; - break; + if ($orderby === "studentlastname") { + $orderby = "u.lastname, u.firstname"; + } else if ($orderby === "studentfirstname") { + $orderby = "u.firstname, u.lastname"; + } else if (in_array($orderby, $customfields)) { // Sort order by current custom user field. + $orderby = $userfieldssql->mappings[$orderby]; } } @@ -668,26 +686,30 @@ public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overvi /** * Get question heading. * - * @param object $attempt an instance of quiz_attempt. - * @param bool $shownames True to show the question name. - * @param bool $showidnumbers True to show the question id number. + * @param stdClass $attempt An instance of quiz_attempt. + * @param bool $shownames True to show the student first/lastnames. + * @param bool $showcustomfields Whether custom field values should be shown. * @return string The string text for the question heading. - * @throws coding_exception */ - protected function get_question_heading($attempt, $shownames, $showidnumbers) { + protected function get_question_heading(stdClass $attempt, bool $shownames, bool $showcustomfields): string { + global $DB; $a = new stdClass(); $a->attempt = $attempt->attempt; $a->fullname = fullname($attempt); - $a->idnumber = s($attempt->idnumber); - - $showidnumbers = $showidnumbers && !empty($attempt->idnumber); + $customfields = []; + foreach ($this->extrauserfields as $field) { + if ($attempt->{s($field)}) { + $customfields[] = $attempt->{s($field)}; + } + } + $a->customfields = trim(implode(', ', (array)$customfields), ' ,'); - if ($shownames && $showidnumbers) { - return get_string('gradingattemptwithidnumber', 'quiz_grading', $a); + if ($shownames && $showcustomfields) { + return get_string('gradingattemptwithcustomfields', 'quiz_grading', $a); } else if ($shownames) { return get_string('gradingattempt', 'quiz_grading', $a); - } else if ($showidnumbers) { - $a->fullname = $attempt->idnumber; + } else if ($showcustomfields) { + $a->fullname = $a->customfields; return get_string('gradingattempt', 'quiz_grading', $a); } else { return ''; diff --git a/mod/quiz/report/grading/tests/behat/grading.feature b/mod/quiz/report/grading/tests/behat/grading.feature index fd0aee8a6df2f..eb9b54a4140dc 100644 --- a/mod/quiz/report/grading/tests/behat/grading.feature +++ b/mod/quiz/report/grading/tests/behat/grading.feature @@ -5,10 +5,19 @@ Feature: Basic use of the Manual grading report I need to use the manual grading report Background: - Given the following "users" exist: - | username | firstname | lastname | email | idnumber | - | teacher1 | T1 | Teacher1 | teacher1@example.com | T1000 | - | student1 | S1 | Student1 | student1@example.com | S1000 | + Given the following "custom profile fields" exist: + | datatype | shortname | name | + | text | username | Username | + | text | email | Email address | + | text | idnumber | ID number | + | text | frog | Favourite frog | + And the following config values are set as admin: + | showuseridentity | username,idnumber,email,profile_field_frog | + + And the following "users" exist: + | username | firstname | lastname | email | idnumber | profile_field_frog | + | teacher1 | T1 | Teacher1 | teacher1@example.com | T1000 | | + | student1 | S1 | Student1 | student1@example.com | S1000 | little yellow frog | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | @@ -52,9 +61,8 @@ Feature: Basic use of the Manual grading report # Go to the grading page. And I click on "update grades" "link" in the "Short answer 001" "table_row" And I should see "Grading attempts 1 to 1 of 1" - # Test the display options. - And I set the field "Order attempts" to "By student ID number" + And I set the field "Order attempts by" to "ID number" And I press "Change options" # General feedback for Short answer 001 displays. @@ -77,16 +85,16 @@ Feature: Basic use of the Manual grading report And I follow "Also show questions that have been graded automatically" And I click on "update grades" "link" in the "Short answer 001" "table_row" And I set the following fields to these values: - | Questions per page | 42 | - | Order attempts | By date | + | Questions per page | 42 | + | Order attempts by | Date | And I press "Change options" And I log out And I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1" And I follow "Also show questions that have been graded automatically" And I click on "update grades" "link" in the "Short answer 001" "table_row" Then the following fields match these values: - | Questions per page | 42 | - | Order attempts | By date | + | Questions per page | 42 | + | Order attempts by | Date | @javascript Scenario: Manual grading settings are validated @@ -111,4 +119,14 @@ Feature: Basic use of the Manual grading report And I set the following fields to these values: | Questions per page | 1 | And I press "Change options" - And I should not see "You must enter a number that is greater than 0." + + @javascript + Scenario: Teacher can see user custom filed columns as additional user identity + Given user "student1" has attempted "Quiz 1" with responses: + | slot | response | + | 1 | Paris | + When I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher1" + And I navigate to "Results > Manual grading" in current page administration + And I follow "Also show questions that have been graded automatically" + And I click on "update grades" "link" in the "Short answer 001" "table_row" + Then I should see "Attempt number 1 for S1 Student1 (student1, S1000, student1@example.com, little yellow frog)" diff --git a/mod/quiz/report/grading/tests/privacy_provider_test.php b/mod/quiz/report/grading/tests/privacy_provider_test.php index 96c3f5ee4efa4..67cc6b79e5f58 100644 --- a/mod/quiz/report/grading/tests/privacy_provider_test.php +++ b/mod/quiz/report/grading/tests/privacy_provider_test.php @@ -73,6 +73,31 @@ public function test_preference_bool_true() { $this->assertEquals(42, $preferences->pagesize->value); $this->assertNotEmpty($preferences->order); - $this->assertEquals('Randomly', $preferences->order->value); + $this->assertEquals('Random', $preferences->order->value); + } + + /** + * Preference does exist using user custom fields. + */ + public function test_preference_bool_true_for_user_customfields() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $customfields = ['username', 'idnumber', 'email', 'profile_field_frog']; + foreach ($customfields as $customfield) { + set_user_preference('quiz_grading_order', $customfield); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_grading'); + + $this->assertNotEmpty($preferences->order); + $this->assertEquals(\core_user\fields::get_display_name($customfield), $preferences->order->value); + } } } From ef9ffcd16da7694c43dee99bd25c6caff1f0eb38 Mon Sep 17 00:00:00 2001 From: PraiseSatan Date: Fri, 12 Nov 2021 07:27:08 +1100 Subject: [PATCH 06/57] MDL-69496 form: Check if element has attributes Checks if a form element has attributes before trying to get the default value. This fixes an error when trying to get the default value for a frozen group which does not have the attributes array. --- lib/formslib.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/formslib.php b/lib/formslib.php index f680f5827da71..3467779d0c42f 100644 --- a/lib/formslib.php +++ b/lib/formslib.php @@ -2250,11 +2250,13 @@ function exportValues($elementList = null){ // iterate over all elements, calling their exportValue() methods foreach (array_keys($this->_elements) as $key) { if ($this->_elements[$key]->isFrozen() && !$this->_elements[$key]->_persistantFreeze) { - $varname = $this->_elements[$key]->_attributes['name']; $value = ''; - // If we have a default value then export it. - if (isset($this->_defaultValues[$varname])) { - $value = $this->prepare_fixed_value($varname, $this->_defaultValues[$varname]); + if (isset($this->_elements[$key]->_attributes['name'])) { + $varname = $this->_elements[$key]->_attributes['name']; + // If we have a default value then export it. + if (isset($this->_defaultValues[$varname])) { + $value = $this->prepare_fixed_value($varname, $this->_defaultValues[$varname]); + } } } else { $value = $this->_elements[$key]->exportValue($this->_submitValues, true); From e11332cf908c930de70f1e397f25bf3e5622f9ba Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 26 Oct 2020 12:41:48 +0800 Subject: [PATCH 07/57] MDL-69092 behat: Repeated generator --- lib/tests/behat/behat_data_generators.php | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index eb39c0353e675..539996fdbfb0b 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -92,6 +92,34 @@ public function the_following_entities_exist($entitytype, TableNode $data) { $this->get_instance_for_component($component)->generate_items($entity, $data); } + /** + * Create multiple entities of one entity type. + * + * @Given :count :entitytype exist with the following data: + * + * @param string $entitytype The name of the type entity to add + * @param int $count + * @param TableNode $data + */ + public function the_following_repeated_entities_exist(string $entitytype, int $count, TableNode $data): void { + $rows = $data->getRowsHash(); + + $tabledata = [array_keys($rows)]; + for ($current = 1; $current < $count + 1; $current++) { + $rowdata = []; + foreach ($rows as $fieldname => $fieldtemplate) { + $rowdata[$fieldname] = str_replace('[count]', $current, $fieldtemplate); + } + $tabledata[] = $rowdata; + } + + if (isset($this->movedentitytypes[$entitytype])) { + $entitytype = $this->movedentitytypes[$entitytype]; + } + list($component, $entity) = $this->parse_entity_type($entitytype); + $this->get_instance_for_component($component)->generate_items($entity, new TableNode($tabledata), false); + } + /** * Creates the specified element. * From ec160a91cb2245553d79bbc1a165ac39979e0578 Mon Sep 17 00:00:00 2001 From: Thong Bui Date: Wed, 10 Nov 2021 12:54:24 +0700 Subject: [PATCH 08/57] MDL-72966 File upload: Uncaught Error from JS when uploading the file --- lib/form/dndupload.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/form/dndupload.js b/lib/form/dndupload.js index de60baf4adece..ad8654e0ba64f 100644 --- a/lib/form/dndupload.js +++ b/lib/form/dndupload.js @@ -863,7 +863,8 @@ M.form_dndupload.init = function(Y, options) { */ notifyUploadCompleted: function() { require(['core_form/events'], function(FormEvent) { - FormEvent.notifyUploadCompleted(this.filemanagerhelper.filemanager.get('id')); + const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid; + FormEvent.triggerUploadCompleted(elementId); }.bind(this)); }, @@ -872,7 +873,8 @@ M.form_dndupload.init = function(Y, options) { */ notifyUploadStarted: function() { require(['core_form/events'], function(FormEvent) { - FormEvent.notifyUploadStarted(this.filemanagerhelper.filemanager.get('id')); + const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid; + FormEvent.triggerUploadStarted(elementId); }.bind(this)); }, From 6ca9c2154a1ea92fa949ef43c9dcf7d13c24b790 Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 21 Oct 2021 12:13:36 +0800 Subject: [PATCH 09/57] MDL-70801 core_my: Add a new courses page --- .../behat/tests/behat/i_am_on_page.feature | 8 +- .../policy/tests/behat/acceptances.feature | 10 +-- .../usertours/tests/behat/create_tour.feature | 8 +- .../tests/behat/tour_navigation.feature | 4 +- blocks/moodleblock.class.php | 11 ++- .../configure_block_throughout_site.feature | 2 +- blocks/tests/externallib_test.php | 24 +++++- .../grader/tests/behat/switch_views.feature | 1 - lang/en/admin.php | 1 + lang/en/my.php | 1 + lib/ajax/blocks.php | 1 + lib/blocklib.php | 29 ++++++- lib/classes/event/mycourses_viewed.php | 58 +++++++++++++ lib/classes/navigation/views/primary.php | 4 +- lib/db/install.php | 7 ++ lib/db/upgrade.php | 12 +++ lib/navigationlib.php | 14 ++- lib/outputrenderers.php | 3 +- lib/tests/behat/behat_navigation.php | 3 + my/courses.php | 86 +++++++++++++++++++ my/lib.php | 80 +++++++++++++---- my/templates/dropdown.mustache | 37 ++++++++ my/tests/behat/my_courses.feature | 13 +++ my/upgrade.txt | 4 + theme/boost/classes/boostnavbar.php | 5 +- theme/boost/config.php | 6 ++ theme/boost/lang/en/theme_boost.php | 1 + theme/boost/scss/moodle/core.scss | 10 +++ theme/boost/style/moodle.css | 7 ++ theme/classic/config.php | 6 ++ theme/classic/style/moodle.css | 7 ++ .../behat_theme_classic_behat_course.php | 3 +- version.php | 2 +- 33 files changed, 417 insertions(+), 51 deletions(-) create mode 100644 lib/classes/event/mycourses_viewed.php create mode 100644 my/courses.php create mode 100644 my/templates/dropdown.mustache create mode 100644 my/tests/behat/my_courses.feature create mode 100644 my/upgrade.txt diff --git a/admin/tool/behat/tests/behat/i_am_on_page.feature b/admin/tool/behat/tests/behat/i_am_on_page.feature index 9c60a8b3da929..8f4abc23ac4a8 100644 --- a/admin/tool/behat/tests/behat/i_am_on_page.feature +++ b/admin/tool/behat/tests/behat/i_am_on_page.feature @@ -71,9 +71,9 @@ Feature: Use core page resolvers for the I am on the page steps Then I should see "" Examples: - | description | identifier | shouldsee | - | Admin page | "Admin notifications" | Check for available updates | - | Home page | Homepage | Course overview | + | description | identifier | shouldsee | + | Admin page | "Admin notifications" | Check for available updates | + | Home page | Homepage | Calendar | Scenario Outline: When I am on a named page logged in as When I am on the page logged in as admin @@ -82,4 +82,4 @@ Feature: Use core page resolvers for the I am on the page steps Examples: | description | identifier | shouldsee | | Admin page | "Admin notifications" | Check for available updates | - | Home page | Homepage | Course overview | + | Home page | Homepage | Calendar | diff --git a/admin/tool/policy/tests/behat/acceptances.feature b/admin/tool/policy/tests/behat/acceptances.feature index 85df22dbaa9a5..998049ca794c3 100644 --- a/admin/tool/policy/tests/behat/acceptances.feature +++ b/admin/tool/policy/tests/behat/acceptances.feature @@ -36,7 +36,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users And I press "Next" And I set the field "I agree to the This site policy" to "1" And I press "Next" - And I should see "Course overview" + And I should see "Calendar" And I log out And I log in as "manager" And I press "Next" @@ -82,7 +82,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users And I press "Next" And I set the field "I agree to the This site policy" to "1" And I press "Next" - And I should see "Course overview" + And I should see "Calendar" And I navigate to "Users > Privacy and policies > Manage policies" in site administration And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row" And I click on "Accept This site policy" "link" in the "User One" "table_row" @@ -111,7 +111,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users And I set the field "I agree to the This site policy" to "1" And I set the field "I agree to the This privacy policy" to "1" And I press "Next" - And I should see "Course overview" + And I should see "Calendar" And I log out And I log in as "manager" And I press "Next" @@ -300,7 +300,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users And I press "Next" And I set the field "I agree to the This site policy" to "1" And I press "Next" - And I should see "Course overview" + And I should see "Calendar" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Manage policies" in site administration @@ -311,4 +311,4 @@ Feature: Viewing acceptances reports and accepting on behalf of other users And I press "Continue" And I log out When I log in as "user1" - Then I should see "Course overview" + Then I should see "Calendar" diff --git a/admin/tool/usertours/tests/behat/create_tour.feature b/admin/tool/usertours/tests/behat/create_tour.feature index 92b676f3e6e96..b1e1b3edaac8f 100644 --- a/admin/tool/usertours/tests/behat/create_tour.feature +++ b/admin/tool/usertours/tests/behat/create_tour.feature @@ -20,7 +20,7 @@ Feature: Add a new user tour | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | And I add steps to the "First tour" tour: | targettype | targetvalue_block | Title | Content | - | Block | Course overview | Course overview | This area shows you what's happening in some of your courses | + | Block | Timeline | Timeline | This is the Timeline. All of your upcoming activities can be found here | | Block | Calendar | Calendar | This is the Calendar. All of your assignments and due dates can be found here | And I add steps to the "First tour" tour: | targettype | targetvalue_selector | Title | Content | @@ -28,7 +28,7 @@ Feature: Add a new user tour When I am on homepage Then I should see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful" And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element" - And I should see "This area shows you what's happening in some of your courses" + And I should see "This is the Timeline. All of your upcoming activities can be found here" And I should not see "This is the Calendar. All of your assignments and due dates can be found here" And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element" And I should see "This is the Calendar. All of your assignments and due dates can be found here" @@ -95,7 +95,7 @@ Feature: Add a new user tour | Display in middle of page | Welcome | First step of the Tour | And I add steps to the "Steps tour" tour: | targettype | targetvalue_block | Title | Content | - | Block | Course overview | Course overview | Second step of the Tour | + | Block | Timeline | Timeline | Second step of the Tour | | Block | Calendar | Calendar | Third step of the Tour | When I am on homepage Then I should see "First step of the Tour" @@ -129,7 +129,7 @@ Feature: Add a new user tour | Display in middle of page | Welcome | First step of the Tour | And I add steps to the "Steps tour" tour: | targettype | targetvalue_block | Title | Content | - | Block | Course overview | Course overview | Second step of the Tour | + | Block | Timeline | Timeline | Second step of the Tour | | Block | Calendar | Calendar | Third step of the Tour | When I am on homepage Then I should see "First step of the Tour" diff --git a/admin/tool/usertours/tests/behat/tour_navigation.feature b/admin/tool/usertours/tests/behat/tour_navigation.feature index 5e577d10c4002..12123d2322669 100644 --- a/admin/tool/usertours/tests/behat/tour_navigation.feature +++ b/admin/tool/usertours/tests/behat/tour_navigation.feature @@ -53,14 +53,14 @@ Feature: Steps can be navigated within a tour | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | And I add steps to the "First tour" tour: | targettype | targetvalue_block | Title | Content | - | Block | Course overview | Course overview | This area shows you what's happening in some of your courses | + | Block | Timeline | Timeline | This is the Timeline. All of your upcoming activities can be found here | | Block | Calendar | Calendar | This is the Calendar. All of your assignments and due dates can be found here | When I am on homepage Then I should see "Skip tour" And I should see "Next (1/3)" And I click on "Next (1/3)" "button" in the "Welcome" "dialogue" And I should see "Skip tour" - And I click on "Next (2/3)" "button" in the "Course overview" "dialogue" + And I click on "Next (2/3)" "button" in the "Timeline" "dialogue" And I should see "End tour" @javascript diff --git a/blocks/moodleblock.class.php b/blocks/moodleblock.class.php index 76de67eef6af5..d50478fecb70b 100644 --- a/blocks/moodleblock.class.php +++ b/blocks/moodleblock.class.php @@ -249,7 +249,7 @@ public function get_content_for_output($output) { $this->arialabel = $bc->arialabel; } - if ($this->page->user_is_editing()) { + if ($this->page->user_is_editing() && $this->instance_can_be_edited()) { $bc->controls = $this->page->blocks->edit_controls($this); } else { // we must not use is_empty on hidden blocks @@ -692,6 +692,15 @@ public function instance_can_be_collapsed() { return true; } + /** + * If overridden and set to false by the block it will not be editable. + * + * @return bool + */ + public function instance_can_be_edited() { + return true; + } + /** @callback callback functions for comments api */ public static function comment_template($options) { $ret = <<get_record('my_pages', array('userid' => null, 'name' => MY_PAGE_DEFAULT, 'private' => true)); // Get the expected default blocks. - $alldefaultblocksordered = $DB->get_records_menu('block_instances', - array('pagetypepattern' => 'my-index'), 'defaultregion, defaultweight ASC', 'id, blockname'); + $alldefaultblocksordered = $DB->get_records_menu( + 'block_instances', + array('pagetypepattern' => 'my-index', 'subpagepattern' => $systempage->id), + 'defaultregion, defaultweight ASC', + 'id, blockname' + ); $this->setUser($user); @@ -368,8 +373,13 @@ public function test_get_dashboard_blocks_default_dashboard_including_sticky_blo $user = $this->getDataGenerator()->create_user(); $PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set. + $systempage = $DB->get_record('my_pages', array('userid' => null, 'name' => MY_PAGE_DEFAULT, 'private' => true)); // Get the expected default blocks. - $alldefaultblocks = $DB->get_records_menu('block_instances', array('pagetypepattern' => 'my-index'), '', 'id, blockname'); + $alldefaultblocks = $DB->get_records_menu( + 'block_instances', array('pagetypepattern' => 'my-index', 'subpagepattern' => $systempage->id), + '', + 'id, blockname' + ); // Now, add a sticky block. $page = new moodle_page(); @@ -411,8 +421,14 @@ public function test_get_dashboard_blocks_custom_user_dashboard() { $user = $this->getDataGenerator()->create_user(); $PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set. + $systempage = $DB->get_record('my_pages', array('userid' => null, 'name' => MY_PAGE_DEFAULT, 'private' => true)); // Get the expected default blocks. - $alldefaultblocks = $DB->get_records_menu('block_instances', array('pagetypepattern' => 'my-index'), '', 'id, blockname'); + $alldefaultblocks = $DB->get_records_menu( + 'block_instances', + array('pagetypepattern' => 'my-index', 'subpagepattern' => $systempage->id), + '', + 'id, blockname' + ); // Add a custom block. $page = new moodle_page(); diff --git a/grade/report/grader/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature index fa65ba83ef95d..98b764b86c173 100644 --- a/grade/report/grader/tests/behat/switch_views.feature +++ b/grade/report/grader/tests/behat/switch_views.feature @@ -38,7 +38,6 @@ Feature: We can change what we are viewing on the grader report And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1" And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2" And I press "Save changes" - And I turn editing mode off @javascript Scenario: View and minimise the grader report containing hidden activities diff --git a/lang/en/admin.php b/lang/en/admin.php index 589b2dc262030..460fe06e21fad 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -858,6 +858,7 @@ $string['modulesecurity'] = 'Module security'; $string['multilangforceold'] = 'Force old multilang syntax: <span> without the class="multilang" and <lang>'; $string['mustenablestats'] = 'Statistics have not yet been enabled on this site.'; +$string['mycourses'] = 'My courses'; $string['mycoursesperpage'] = 'Number of courses'; $string['mydashboard'] = 'System default dashboard'; $string['mymoodle'] = 'Dashboard'; diff --git a/lang/en/my.php b/lang/en/my.php index 97a6bfa46359e..cc0b8f7356706 100644 --- a/lang/en/my.php +++ b/lang/en/my.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['coursemanagementoptions'] = 'Course management options'; $string['mymoodle'] = 'Dashboard'; $string['nocourses'] = 'No course information to show.'; $string['noguest'] = 'The Dashboard page is not available to guest users'; diff --git a/lib/ajax/blocks.php b/lib/ajax/blocks.php index d12eccea684be..7d1a7625d90a2 100644 --- a/lib/ajax/blocks.php +++ b/lib/ajax/blocks.php @@ -60,6 +60,7 @@ $pagetype = explode('-', $pagetype); switch ($pagetype[0]) { case 'my': + case 'mycourses': $PAGE->set_blocks_editing_capability('moodle/my:manageblocks'); break; case 'user': diff --git a/lib/blocklib.php b/lib/blocklib.php index b16fa61bff8de..56feb707a4a05 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -2647,7 +2647,30 @@ function blocks_add_default_system_blocks() { $subpagepattern = null; } - $newblocks = array('timeline', 'private_files', 'badges', 'calendar_month'); - $newcontent = array('myoverview'); - $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern); + if ($defaultmycoursespage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__courses', 'private' => 0))) { + $mycoursesubpagepattern = $defaultmycoursespage->id; + } else { + $mycoursesubpagepattern = null; + } + + $page->blocks->add_blocks([ + BLOCK_POS_RIGHT => [ + 'private_files', + 'badges', + ], + 'content' => [ + 'timeline', + 'calendar_month', + ]], + 'my-index', + $subpagepattern + ); + + $page->blocks->add_blocks([ + 'content' => [ + 'myoverview' + ]], + 'my-index', + $mycoursesubpagepattern + ); } diff --git a/lib/classes/event/mycourses_viewed.php b/lib/classes/event/mycourses_viewed.php new file mode 100644 index 0000000000000..d1d435f2bcb37 --- /dev/null +++ b/lib/classes/event/mycourses_viewed.php @@ -0,0 +1,58 @@ +. + +namespace core\event; + +/** + * My courses viewed event class. + * + * Class for event to be triggered when a user views their My courses page. + * + * @package core + * @copyright 2021 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mycourses_viewed extends base { + + /** + * Init method. + * + * @return void + */ + protected function init(): void { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description(): string { + return "The user with id '$this->userid' has viewed their my courses page"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name(): string { + return get_string('eventmycoursesviewed', 'core'); + } + +} diff --git a/lib/classes/navigation/views/primary.php b/lib/classes/navigation/views/primary.php index e7fd0a125ef72..d970f337c8121 100644 --- a/lib/classes/navigation/views/primary.php +++ b/lib/classes/navigation/views/primary.php @@ -57,8 +57,8 @@ public function initialise(): void { } } - // Add a dummy mycourse link to a mycourses page. - $this->add(get_string('mycourses'), new \moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses'); + // Add the mycourses link. + $this->add(get_string('mycourses'), new \moodle_url('/my/courses.php'), self::TYPE_ROOTNODE, null, 'courses'); // Add the site admin node. We are using the settingsnav so as to avoid rechecking permissions again. $settingsnav = $this->page->settingsnav; diff --git a/lib/db/install.php b/lib/db/install.php index e1ba215072d55..db1041e4700fc 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -310,6 +310,13 @@ function xmldb_main_install() { $mypage->private = 1; $DB->insert_record('my_pages', $mypage); + $mycoursespage = new stdClass(); + $mycoursespage->userid = null; + $mycoursespage->name = '__courses'; + $mycoursespage->private = 0; + $mycoursespage->sortorder = 0; + $DB->insert_record('my_pages', $mycoursespage); + // Set a sensible default sort order for the most-used question types. set_config('multichoice_sortorder', 1, 'question'); set_config('truefalse_sortorder', 2, 'question'); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index c76b4d959f9c7..38a3954ccc990 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3145,5 +3145,17 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2021110800.03); } + if ($oldversion < 2021111200.01) { + + $mycoursespage = new stdClass(); + $mycoursespage->userid = null; + $mycoursespage->name = '__courses'; + $mycoursespage->private = 0; + $mycoursespage->sortorder = 0; + $DB->insert_record('my_pages', $mycoursespage); + + upgrade_main_savepoint(true, 2021111200.01); + } + return true; } diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 45c324eb39913..87a7a1409e855 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1328,7 +1328,14 @@ public function initialise() { $this->rootnodes['site'] = $this->add_course($SITE); $this->rootnodes['myprofile'] = $this->add(get_string('profile'), null, self::TYPE_USER, null, 'myprofile'); $this->rootnodes['currentcourse'] = $this->add(get_string('currentcourse'), null, self::TYPE_ROOTNODE, null, 'currentcourse'); - $this->rootnodes['mycourses'] = $this->add(get_string('mycourses'), null, self::TYPE_ROOTNODE, null, 'mycourses', new pix_icon('i/course', '')); + $this->rootnodes['mycourses'] = $this->add( + get_string('mycourses'), + new moodle_url('/my/courses.php'), + self::TYPE_ROOTNODE, + null, + 'mycourses', + new pix_icon('i/course', '') + ); $this->rootnodes['courses'] = $this->add(get_string('courses'), new moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses'); if (!core_course_category::user_top()) { $this->rootnodes['courses']->hide(); @@ -1521,7 +1528,7 @@ public function initialise() { foreach ($this->rootnodes as $node) { // Dont remove the home node /** @var navigation_node $node */ - if (!in_array($node->key, ['home', 'myhome']) && !$node->has_children() && !$node->isactive) { + if (!in_array($node->key, ['home', 'mycourses', 'myhome']) && !$node->has_children() && !$node->isactive) { $node->remove(); } } @@ -2880,6 +2887,9 @@ public function add_front_page_course_essentials(navigation_node $coursenode, st // This required as there are not other guaranteed nodes that may be loaded. $coursenode->add('frontpageloaded', null, self::TYPE_CUSTOM, null, 'frontpageloaded')->display = false; + // Add My courses to the site pages within the navigation structure so the block can read it. + $coursenode->add(get_string('mycourses'), new moodle_url('/my/courses.php'), self::TYPE_CUSTOM, null, 'mycourses'); + // Participants. if ($navoptions->participants) { $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), self::TYPE_CUSTOM, get_string('participants'), 'participants'); diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index ba37129b867af..16ef03f41ef6c 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4372,7 +4372,8 @@ public function full_header() { $pagetype = $this->page->pagetype; $homepage = get_home_page(); $homepagetype = null; - if ($homepage == HOMEPAGE_MY) { + // Add a special case since /my/courses is a part of the /my subsystem. + if ($homepage == HOMEPAGE_MY && $this->page->title !== get_string('mycourses')) { $homepagetype = 'my-index'; } else if ($homepage == HOMEPAGE_SITE) { $homepagetype = 'site-index'; diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php index d48ac3c759d61..88f56cb62bb02 100644 --- a/lib/tests/behat/behat_navigation.php +++ b/lib/tests/behat/behat_navigation.php @@ -717,6 +717,9 @@ protected function resolve_core_page_url(string $name): moodle_url { case 'Homepage': return new moodle_url('/'); + case 'My courses': + return new moodle_url('/my/courses.php'); + case 'Admin notifications': return new moodle_url('/admin/'); diff --git a/my/courses.php b/my/courses.php new file mode 100644 index 0000000000000..0351e413ce483 --- /dev/null +++ b/my/courses.php @@ -0,0 +1,86 @@ +. + +/** + * My Courses. + * + * - each user can currently have their own page (cloned from system and then customised) + * - only the user can see their own dashboard + * - users can add any blocks they want + * + * @package core + * @subpackage my + * @copyright 2021 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../config.php'); +require_once($CFG->dirroot . '/my/lib.php'); + +redirect_if_major_upgrade_required(); + +require_login(); + +$hassiteconfig = has_capability('moodle/site:config', context_system::instance()); +if ($hassiteconfig && moodle_needs_upgrading()) { + redirect(new moodle_url('/admin/index.php')); +} + +$context = context_system::instance(); + +// Get the My Moodle page info. Should always return something unless the database is broken. +if (!$currentpage = my_get_page(null, MY_PAGE_PUBLIC, MY_PAGE_COURSES)) { + throw new Exception('mymoodlesetup'); +} + +// Start setting up the page. +$PAGE->set_context($context); +$PAGE->set_url('/my/courses.php'); +$PAGE->add_body_classes(['limitedwidth', 'page-mycourses']); +$PAGE->set_pagelayout('mycourses'); +$PAGE->has_secondary_navigation_setter(false); + +$PAGE->set_pagetype('my-index'); +$PAGE->set_subpage($currentpage->id); +$PAGE->set_title(get_string('mycourses')); +$PAGE->set_heading(get_string('mycourses')); +// Force the add block out of the default area. +$PAGE->theme->addblockposition = BLOCK_ADDBLOCK_POSITION_CUSTOM; + +// Add course management if the user has the capabilities for it. +$coursecat = core_course_category::user_top(); +if ($coursecat->can_create_course() || $coursecat->has_manage_capability()) { + $data = [ + 'newcourseurl' => new moodle_url('/course/edit.php', ['category' => $coursecat->id]), + 'manageurl' => new moodle_url('/course/management.php', ['categoryid' => $coursecat->id]), + ]; + $PAGE->add_header_action($OUTPUT->render_from_template('my/dropdown', $data)); +} + +echo $OUTPUT->header(); + +if (core_userfeedback::should_display_reminder()) { + core_userfeedback::print_reminder_block(); +} + +echo $OUTPUT->custom_block_region('content'); + +echo $OUTPUT->footer(); + +// Trigger dashboard has been viewed event. +$eventparams = array('context' => $context); +$event = \core\event\mycourses_viewed::create($eventparams); +$event->trigger(); diff --git a/my/lib.php b/my/lib.php index 590fe95d67c3b..9002df0b1c649 100644 --- a/my/lib.php +++ b/my/lib.php @@ -29,41 +29,70 @@ define('MY_PAGE_PUBLIC', 0); define('MY_PAGE_PRIVATE', 1); +define('MY_PAGE_DEFAULT', '__default'); +define('MY_PAGE_COURSES', '__courses'); require_once("$CFG->libdir/blocklib.php"); -/* +/** * For a given user, this returns the $page information for their My Moodle page * + * @param int|null $userid the id of the user whose page should be retrieved + * @param int|null $private either MY_PAGE_PRIVATE or MY_PAGE_PUBLIC + * @param string|null $pagename Differentiate between standard /my or /courses pages. */ -function my_get_page($userid, $private=MY_PAGE_PRIVATE) { +function my_get_page(?int $userid, int $private = MY_PAGE_PRIVATE, string $pagename = MY_PAGE_DEFAULT) { global $DB, $CFG; if (empty($CFG->forcedefaultmymoodle) && $userid) { // Ignore custom My Moodle pages if admin has forced them // Does the user have their own page defined? If so, return it. - if ($customised = $DB->get_record('my_pages', array('userid' => $userid, 'private' => $private))) { + if ($customised = $DB->get_record( + 'my_pages', + array('userid' => $userid, 'private' => $private, 'name' => $pagename), + '*', + IGNORE_MULTIPLE + )) { return $customised; } } // Otherwise return the system default page - return $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => $private)); + return $DB->get_record('my_pages', array('userid' => null, 'name' => $pagename, 'private' => $private), '*', IGNORE_MULTIPLE); } -/* +/** * This copies a system default page to the current user * + * @param int $userid the id of the user whose page should be reset + * @param int $private either MY_PAGE_PRIVATE or MY_PAGE_PUBLIC + * @param string $pagetype either my-index or user-profile + * @param string $pagename Differentiate between standard /my or /courses pages. */ -function my_copy_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index') { +function my_copy_page( + int $userid, + int $private = MY_PAGE_PRIVATE, + string $pagetype = 'my-index', + string $pagename = MY_PAGE_DEFAULT +) { global $DB; - if ($customised = $DB->get_record('my_pages', array('userid' => $userid, 'private' => $private))) { + if ($customised = $DB->get_record( + 'my_pages', + array('userid' => $userid, 'name' => $pagename, 'private' => $private), + '*', + IGNORE_MULTIPLE + )) { return $customised; // We're done! } // Get the system default page - if (!$systempage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => $private))) { + if (!$systempage = $DB->get_record( + 'my_pages', + array('userid' => null, 'name' => $pagename, 'private' => $private), + '*', + IGNORE_MULTIPLE + )) { return false; // error } @@ -117,18 +146,24 @@ function my_copy_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index') { return $page; } -/* +/** * For a given user, this deletes their My Moodle page and returns them to the system default. * * @param int $userid the id of the user whose page should be reset * @param int $private either MY_PAGE_PRIVATE or MY_PAGE_PUBLIC * @param string $pagetype either my-index or user-profile + * @param string $pagename Differentiate between standard /my or /courses pages. * @return mixed system page, or false on error */ -function my_reset_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index') { +function my_reset_page( + int $userid, + int $private = MY_PAGE_PRIVATE, + string $pagetype='my-index', + string $pagename = MY_PAGE_DEFAULT +) { global $DB, $CFG; - $page = my_get_page($userid, $private); + $page = my_get_page($userid, $private, $pagename); if ($page->userid == $userid) { $context = context_user::instance($userid); if ($blocks = $DB->get_records('block_instances', array('parentcontextid' => $context->id, @@ -140,11 +175,16 @@ function my_reset_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index') } } $DB->delete_records('block_positions', ['subpage' => $page->id, 'pagetype' => $pagetype, 'contextid' => $context->id]); - $DB->delete_records('my_pages', array('id' => $page->id)); + $DB->delete_records('my_pages', array('id' => $page->id, 'name' => $pagename)); } // Get the system default page - if (!$systempage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => $private))) { + if (!$systempage = $DB->get_record( + 'my_pages', + array('userid' => null, 'name' => $pagename, 'private' => $private), + '*', + IGNORE_MULTIPLE + )) { return false; // error } @@ -166,10 +206,16 @@ function my_reset_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index') * * @param int $private Either MY_PAGE_PRIVATE or MY_PAGE_PUBLIC. * @param string $pagetype Either my-index or user-profile. - * @param progress_bar $progressbar A progress bar to update. + * @param progress_bar|null $progressbar A progress bar to update. + * @param string $pagename Differentiate between standard /my or /courses pages. * @return void */ -function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my-index', $progressbar = null) { +function my_reset_page_for_all_users( + int $private = MY_PAGE_PRIVATE, + string $pagetype = 'my-index', + ?progress_bar $progressbar = null, + string $pagename = MY_PAGE_DEFAULT +) { global $DB; // This may take a while. Raise the execution time limit. @@ -199,12 +245,14 @@ function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my AND bi.pagetypepattern = :pagetypepattern AND (bi.subpagepattern IS NULL OR bi.subpagepattern = " . $DB->sql_concat("''", 'p.id') . ") WHERE p.private = :private + AND p.name = :name AND p.userid $infragment"; $params = array_merge([ 'private' => $private, 'usercontextlevel' => CONTEXT_USER, - 'pagetypepattern' => $pagetype + 'pagetypepattern' => $pagetype, + 'name' => $pagename ], $inparams); $blockids = $DB->get_fieldset_sql($sql, $params); diff --git a/my/templates/dropdown.mustache b/my/templates/dropdown.mustache new file mode 100644 index 0000000000000..d1ff8f5a83fea --- /dev/null +++ b/my/templates/dropdown.mustache @@ -0,0 +1,37 @@ +{{! + 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 . +}} +{{! + @template my/dropdown + + Simple dropdown for the my/courses page + + Example context (json): + { + "newcourseurl": "https://moodle.test/course/edit.php?category=1", + "manageurl": "https://moodle.test/course/management.php?categoryid=1" + } +}} + diff --git a/my/tests/behat/my_courses.feature b/my/tests/behat/my_courses.feature new file mode 100644 index 0000000000000..bddf5f1e542b4 --- /dev/null +++ b/my/tests/behat/my_courses.feature @@ -0,0 +1,13 @@ +@core @core_my +Feature: Run tests over my courses. + + Scenario: Admin can add new courses or manage them from my courses + Given I am on the "My courses" page logged in as "admin" + And I click on "Course management options" "link" + And I click on "New course" "link" + And I wait to be redirected + Then I should see "Add a new course" + And I am on the "My courses" page + And I click on "Course management options" "link" + And I click on "Manage courses" "link" + And I should see "Course and category management" diff --git a/my/upgrade.txt b/my/upgrade.txt new file mode 100644 index 0000000000000..36979069d8960 --- /dev/null +++ b/my/upgrade.txt @@ -0,0 +1,4 @@ +This files describes changes in the my system. +=== 4.0 === +* Introduce a new /courses page +* Tighten up the typing within the lib file diff --git a/theme/boost/classes/boostnavbar.php b/theme/boost/classes/boostnavbar.php index 9ef725d49c7b1..fb922ed8e2e5f 100644 --- a/theme/boost/classes/boostnavbar.php +++ b/theme/boost/classes/boostnavbar.php @@ -64,10 +64,9 @@ protected function prepare_nodes_for_boost(): void { // Set the designated one path for courses. $mycoursesnode = $this->get_item('mycourses'); if (!is_null($mycoursesnode)) { - // TODO: Once MDL-70801 lands point this to the new page. - $url = new \moodle_url('/course/'); + $url = new \moodle_url('/my/courses.php'); $mycoursesnode->action = $url; - $mycoursesnode->text = get_string('courses'); + $mycoursesnode->text = get_string('mycourses'); } $this->remove_no_link_items(); diff --git a/theme/boost/config.php b/theme/boost/config.php index 141a07d140d97..ee61566f096cb 100644 --- a/theme/boost/config.php +++ b/theme/boost/config.php @@ -78,6 +78,12 @@ 'regions' => array('side-pre'), 'defaultregion' => 'side-pre', ), + // My courses page. + 'mycourses' => array( + 'file' => 'columns2.php', + 'regions' => array('content'), + 'defaultregion' => 'content', + ), // My dashboard page. 'mydashboard' => array( 'file' => 'columns2.php', diff --git a/theme/boost/lang/en/theme_boost.php b/theme/boost/lang/en/theme_boost.php index 16f509b45d958..438b148d33c0d 100644 --- a/theme/boost/lang/en/theme_boost.php +++ b/theme/boost/lang/en/theme_boost.php @@ -49,6 +49,7 @@ $string['rawscsspre'] = 'Raw initial SCSS'; $string['rawscsspre_desc'] = 'In this field you can provide initialising SCSS code, it will be injected before everything else. Most of the time you will use this setting to define variables.'; $string['region-side-pre'] = 'Right'; +$string['region-content'] = 'Content'; $string['showfooter'] = 'Show footer'; $string['privacy:metadata:preference:draweropennav'] = 'The user\'s preference for hiding or showing the drawer menu navigation.'; $string['privacy:drawernavclosed'] = 'The current preference for the navigation drawer is closed.'; diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 71ebbc0ba6bb7..502a9dbda7131 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2875,3 +2875,13 @@ body.dragging { } } } + +.page-mycourses { + #region-main { + padding: 0; + } + #region-main, + .block_myoverview { + border: 0; + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 0926fb7ae73ed..d47c52657ca65 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -12211,6 +12211,13 @@ body.dragging .dragging { .collapse-list .collapse-list-item-content .collapse-list-item { padding-left: calc(1rem * 3); } +.page-mycourses #region-main { + padding: 0; } + +.page-mycourses #region-main, +.page-mycourses .block_myoverview { + border: 0; } + .icon { font-size: 16px; width: 16px; diff --git a/theme/classic/config.php b/theme/classic/config.php index da34422bc66cf..a4d584931bd35 100644 --- a/theme/classic/config.php +++ b/theme/classic/config.php @@ -72,6 +72,12 @@ 'regions' => array('side-pre'), 'defaultregion' => 'side-pre', ), + // My courses page. + 'mycourses' => array( + 'file' => 'columns.php', + 'regions' => array('content'), + 'defaultregion' => 'content', + ), // My dashboard page. 'mydashboard' => array( 'file' => 'columns.php', diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index c46cd2bdbaf7e..c5ea2e1b5b2d8 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12211,6 +12211,13 @@ body.dragging .dragging { .collapse-list .collapse-list-item-content .collapse-list-item { padding-left: calc(1rem * 3); } +.page-mycourses #region-main { + padding: 0; } + +.page-mycourses #region-main, +.page-mycourses .block_myoverview { + border: 0; } + .icon { font-size: 16px; width: 16px; diff --git a/theme/classic/tests/behat/behat_theme_classic_behat_course.php b/theme/classic/tests/behat/behat_theme_classic_behat_course.php index 0cb1ddbb1752f..e7c787e9f0473 100644 --- a/theme/classic/tests/behat/behat_theme_classic_behat_course.php +++ b/theme/classic/tests/behat/behat_theme_classic_behat_course.php @@ -43,7 +43,8 @@ class behat_theme_classic_behat_course extends behat_course { public function i_navigate_to_course_participants() { $coursestr = behat_context_helper::escape(get_string('courses')); $mycoursestr = behat_context_helper::escape(get_string('mycourses')); - $xpath = "//div[contains(@class,'block')]//li[p/*[string(.)=$coursestr or string(.)=$mycoursestr]]"; + $xpath = "//div[contains(@class,'block')]//li[contains(@class,'contains_branch')]" . + "[p/*[string(.)=$coursestr or string(.)=$mycoursestr]]"; $this->execute('behat_general::i_click_on_in_the', [get_string('participants'), 'link', $xpath, 'xpath_element']); } diff --git a/version.php b/version.php index e30705c2c9c9a..25ceefd457081 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021111200.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021111200.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20211112)'; // Human-friendly version name From e110e5b33014e60377772a1439cb57f4f7eb45f8 Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 21 Oct 2021 12:12:48 +0800 Subject: [PATCH 10/57] MDL-70801 block_myoverview: Upgrade steps to relocate the block --- blocks/myoverview/block_myoverview.php | 13 ++++ blocks/myoverview/db/upgrade.php | 74 +++++++++++++++++++ blocks/myoverview/lib.php | 2 +- .../block_myoverview_adminsettings.feature | 32 ++++---- .../block_myoverview_customfield.feature | 24 +++--- .../behat/block_myoverview_dashboard.feature | 56 +++++++------- .../behat/block_myoverview_favourite.feature | 6 +- .../behat/block_myoverview_hidden.feature | 12 +-- ...k_myoverview_pagelimit_persistence.feature | 4 +- .../behat/block_myoverview_pagination.feature | 15 ++-- .../behat/block_myoverview_progress.feature | 7 +- .../behat/block_myoverview_search.feature | 6 +- blocks/myoverview/tests/myoverview_test.php | 6 +- blocks/myoverview/version.php | 2 +- 14 files changed, 173 insertions(+), 86 deletions(-) diff --git a/blocks/myoverview/block_myoverview.php b/blocks/myoverview/block_myoverview.php index 09a82fe2a5ee6..377e84292ab32 100644 --- a/blocks/myoverview/block_myoverview.php +++ b/blocks/myoverview/block_myoverview.php @@ -113,5 +113,18 @@ public function get_config_for_external() { 'plugin' => $configs, ]; } + + /** + * Disable block editing on the my courses page. + * + * @return boolean + */ + public function instance_can_be_edited() { + if ($this->page->blocks->is_known_region(BLOCK_POS_LEFT) || $this->page->blocks->is_known_region(BLOCK_POS_RIGHT)) { + return true; + } else { + return false; + } + } } diff --git a/blocks/myoverview/db/upgrade.php b/blocks/myoverview/db/upgrade.php index 823981818bf99..47160e4684773 100644 --- a/blocks/myoverview/db/upgrade.php +++ b/blocks/myoverview/db/upgrade.php @@ -25,6 +25,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/my/lib.php'); + /** * Upgrade code for the MyOverview block. * @@ -81,5 +83,77 @@ function xmldb_block_myoverview_upgrade($oldversion) { // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2021052504) { + /** + * Small helper function for this version upgrade to delete instances of this block. + * + * @param stdClass $instance DB record of a block that we need to delete within Moodle. + */ + function delete_block_instance(stdClass $instance) { + global $DB; + if ($instance) { + list($sql, $params) = $DB->get_in_or_equal($instance->id, SQL_PARAMS_NAMED); + $params['contextlevel'] = CONTEXT_BLOCK; + $DB->delete_records_select('context', "contextlevel=:contextlevel AND instanceid " . $sql, $params); + $DB->delete_records('block_positions', ['blockinstanceid' => $instance->id]); + $DB->delete_records('block_instances', ['id' => $instance->id]); + $DB->delete_records_list('user_preferences', 'name', + ['block' . $instance->id . 'hidden', 'docked_block_instance_' . $instance->id]); + } + } + + // Delete the default indexsys version of the block. + $mysubpagepattern = $DB->get_record( + 'my_pages', + ['userid' => null, 'name' => MY_PAGE_DEFAULT, 'private' => MY_PAGE_PRIVATE], + 'id', + IGNORE_MULTIPLE + )->id; + $instances = $DB->get_records('block_instances', ['blockname' => 'myoverview', + 'pagetypepattern' => 'my-index', 'subpagepattern' => $mysubpagepattern]); + foreach ($instances as $instance) { + delete_block_instance($instance); + } + + // Begin looking for any and all instances of course overview in customised /my pages. + $pageselect = 'name = :name and private = :private and userid IS NOT NULL'; + $pageparams['name'] = MY_PAGE_DEFAULT; + $pageparams['private'] = MY_PAGE_PRIVATE; + + $pages = $DB->get_recordset_select('my_pages', $pageselect, $pageparams); + foreach ($pages as $page) { + $blocksql = 'blockname = :blockname and pagetypepattern = :pagetypepattern and subpagepattern = :subpagepattern'; + $blockparams['blockname'] = 'myoverview'; + $blockparams['pagetypepattern'] = 'my-index'; + $blockparams['subpagepattern'] = $page->id; + $instances = $DB->get_records_select('block_instances', $blocksql, $blockparams); + foreach ($instances as $instance) { + delete_block_instance($instance); + } + } + $pages->close(); + + // Add new instance to the /my/courses.php page. + $subpagepattern = $DB->get_record( + 'my_pages', + ['userid' => null, 'name' => MY_PAGE_COURSES, 'private' => MY_PAGE_PUBLIC], + 'id', + IGNORE_MULTIPLE + )->id; + + // See if this block already somehow exists, it should not but who knows. + if (!$DB->get_record('block_instances', ['blockname' => 'myoverview', + 'pagetypepattern' => 'my-index', 'subpagepattern' => $subpagepattern])) { + $page = new moodle_page(); + $systemcontext = context_system::instance(); + $page->set_context($systemcontext); + // Add the block to the default /my/courses. + $page->blocks->add_region('content'); + $page->blocks->add_block('myoverview', 'content', 0, false, 'my-index', $subpagepattern); + } + + upgrade_block_savepoint(true, 2021052504, 'myoverview', false); + } + return true; } diff --git a/blocks/myoverview/lib.php b/blocks/myoverview/lib.php index 7080d73f4b63a..8352f63ab05a5 100644 --- a/blocks/myoverview/lib.php +++ b/blocks/myoverview/lib.php @@ -101,7 +101,7 @@ function block_myoverview_user_preferences() { $preferences['block_myoverview_user_sort_preference'] = array( 'null' => NULL_NOT_ALLOWED, - 'default' => BLOCK_MYOVERVIEW_SORTING_TITLE, + 'default' => BLOCK_MYOVERVIEW_SORTING_LASTACCESSED, 'type' => PARAM_ALPHA, 'choices' => array( BLOCK_MYOVERVIEW_SORTING_TITLE, diff --git a/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature b/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature index e9356fabd5bdf..92a51af321bd9 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature @@ -32,7 +32,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "All" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" # We have to check for the data attribute instead of the list element text as we would get false positives from the "All (except removed from view)" element otherwise Then "[data-value='allincludinghidden']" "css_element" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -43,7 +43,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "All" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" # We have to check for the data attribute instead of the list element text as we would get false negatives "All (except removed from view)" element otherwise Then "[data-value='allincludinghidden']" "css_element" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -54,7 +54,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "All (except removed from view)" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "All (except removed from view)" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -64,7 +64,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "All (except removed from view)" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" # 'All (except removed from view)' option has been disabled, so the button is falling back to the 'In progress' option which is the next enabled option. And I click on "In progress" "button" in the "Course overview" "block" Then "All (except removed from view)" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -75,7 +75,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "In progress" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "In progress" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -85,7 +85,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "In progress" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "In progress" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -95,7 +95,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Future" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Future" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -105,7 +105,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Future" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Future" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -115,7 +115,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Past" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Past" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -125,7 +125,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Past" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Past" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -135,7 +135,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Starred" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Starred" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -145,7 +145,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Starred" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Starred" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -155,7 +155,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Removed from view" to "1" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Removed from view" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -165,7 +165,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Removed from view" to "0" And I press "Save" And I log out - Then I log in as "student1" + Then I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then "Removed from view" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element" @@ -181,7 +181,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Removed from view" to "0" And I press "Save" And I log out - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" Then "button#groupingdropdown" "css_element" should not exist in the ".block_myoverview" "css_element" And I should see "Course 1" in the "Course overview" "block" And I should see "Course 2" in the "Course overview" "block" @@ -201,7 +201,7 @@ Feature: The my overview block allows admins to easily configure the students' c And I set the field "Removed from view" to "0" And I press "Save" And I log out - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" Then "button#groupingdropdown" "css_element" should not exist in the ".block_myoverview" "css_element" And I should see "Course 2" in the "Course overview" "block" And I should see "Course 3" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/behat/block_myoverview_customfield.feature b/blocks/myoverview/tests/behat/block_myoverview_customfield.feature index c866d5d0c227a..096b136895e65 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_customfield.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_customfield.feature @@ -35,7 +35,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | checkboxfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Checkbox field: Yes" "link" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" @@ -48,7 +48,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | checkboxfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Checkbox field: No" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -61,7 +61,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | datefield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "1 February 2001" "link" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" @@ -74,7 +74,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | datefield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "6 August 1980" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -87,7 +87,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | datefield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "No Date field" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -100,7 +100,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | selectfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" And I should not see "Option 4" in the "Course overview" "block" When I click on "Option 1" "link" in the "Course overview" "block" @@ -114,7 +114,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | selectfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Option 2" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -127,7 +127,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | selectfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "No Select field" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -140,7 +140,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | textfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "fish" "link" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" @@ -153,7 +153,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | textfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "dog" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -166,7 +166,7 @@ Feature: The my overview block allows users to group courses by custom fields Given the following config values are set as admin: | displaygroupingcustomfield | 1 | block_myoverview | | customfiltergrouping | textfield | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "No Text field" "link" in the "Course overview" "block" Then I should not see "Course 1" in the "Course overview" "block" @@ -191,7 +191,7 @@ Feature: The my overview block allows users to group courses by custom fields And I set the field "Visible to" to "Nobody" And I press "Save changes" And I log out - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then I should not see "penguin" in the "Course overview" "block" Then I should not see "No text field" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature index b1dcc7bd0b231..ce381bcb1c1a8 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature @@ -27,7 +27,7 @@ Feature: The my overview block allows users to easily access their courses | student1 | C5 | student | Scenario: View past courses - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Past" "link" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" @@ -37,7 +37,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View future courses - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Future" "link" in the "Course overview" "block" Then I should see "Course 5" in the "Course overview" "block" @@ -47,7 +47,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 4" in the "Course overview" "block" Scenario: View inprogress courses - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "In progress" "link" in the "Course overview" "block" Then I should see "Course 2" in the "Course overview" "block" @@ -57,7 +57,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View all (except removed) courses - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" @@ -70,7 +70,7 @@ Feature: The my overview block allows users to easily access their courses Given the following config values are set as admin: | config | value | plugin | | displaygroupingallincludinghidden | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except removed from view)" element instead When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block" @@ -81,7 +81,7 @@ Feature: The my overview block allows users to easily access their courses Then I should see "Course 5" in the "Course overview" "block" Scenario: View inprogress courses - test persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" And I click on "In progress" "link" in the "Course overview" "block" And I reload the page @@ -93,7 +93,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View all (except removed) courses - w/ persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I reload the page @@ -105,7 +105,7 @@ Feature: The my overview block allows users to easily access their courses Then I should see "Course 5" in the "Course overview" "block" Scenario: View past courses - w/ persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Past" "link" in the "Course overview" "block" And I reload the page @@ -117,7 +117,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View future courses - w/ persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Future" "link" in the "Course overview" "block" And I reload the page @@ -129,7 +129,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 4" in the "Course overview" "block" Scenario: View favourite courses - w/ persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" And I click on "All (except removed from view)" "button" in the "Course overview" "block" @@ -143,7 +143,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: List display persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" And I click on "List" "link" in the "Course overview" "block" And I reload the page @@ -151,7 +151,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-display='list']" "css_element" in the "Course overview" "block" should be visible Scenario: Cards display persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" And I click on "Card" "link" in the "Course overview" "block" And I reload the page @@ -159,7 +159,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-display='card']" "css_element" in the "Course overview" "block" should be visible Scenario: Summary display persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" And I click on "Summary" "link" in the "Course overview" "block" And I reload the page @@ -167,7 +167,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-display='summary']" "css_element" in the "Course overview" "block" should be visible Scenario: Course name sort persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "sortingdropdown" "button" in the "Course overview" "block" And I click on "Course name" "link" in the "Course overview" "block" And I reload the page @@ -175,7 +175,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-sort='fullname']" "css_element" in the "Course overview" "block" should be visible Scenario: Last accessed sort persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "sortingdropdown" "button" in the "Course overview" "block" And I click on "Last accessed" "link" in the "Course overview" "block" And I reload the page @@ -183,7 +183,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible Scenario: Short name sort persistence - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on "sortingdropdown" "button" in the "Course overview" "block" Then I should not see "Short name" in the "Course overview" "block" When the following config values are set as admin: @@ -197,7 +197,7 @@ Feature: The my overview block allows users to easily access their courses And "[data-sort='shortname']" "css_element" in the "Course overview" "block" should be visible Scenario: View inprogress courses with hide persistent functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "In progress" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -210,7 +210,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View past courses with hide persistent functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Past" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 1')]" "xpath_element" @@ -223,7 +223,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 5" in the "Course overview" "block" Scenario: View future courses with hide persistent functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "Future" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" @@ -236,7 +236,7 @@ Feature: The my overview block allows users to easily access their courses And I should not see "Course 4" in the "Course overview" "block" Scenario: View all (except hidden) courses with hide persistent functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" @@ -252,7 +252,7 @@ Feature: The my overview block allows users to easily access their courses Given the following config values are set as admin: | config | value | plugin | | displaygroupingallincludinghidden | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except removed from view)" element instead When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block" @@ -268,7 +268,7 @@ Feature: The my overview block allows users to easily access their courses Scenario: Show course category in cards display Given the following config values are set as admin: | displaycategories | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "Card" "link" in the "Course overview" "block" Then I should see "Category 1" in the "Course overview" "block" @@ -276,7 +276,7 @@ Feature: The my overview block allows users to easily access their courses Scenario: Show course category in list display Given the following config values are set as admin: | displaycategories | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "List" "link" in the "Course overview" "block" Then I should see "Category 1" in the "Course overview" "block" @@ -284,7 +284,7 @@ Feature: The my overview block allows users to easily access their courses Scenario: Show course category in summary display Given the following config values are set as admin: | displaycategories | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "Summary" "link" in the "Course overview" "block" Then I should see "Category 1" in the "Course overview" "block" @@ -292,7 +292,7 @@ Feature: The my overview block allows users to easily access their courses Scenario: Hide course category in cards display Given the following config values are set as admin: | displaycategories | 0 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "Card" "link" in the "Course overview" "block" Then I should not see "Category 1" in the "Course overview" "block" @@ -300,7 +300,7 @@ Feature: The my overview block allows users to easily access their courses Scenario: Hide course category in list display Given the following config values are set as admin: | displaycategories | 0 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "List" "link" in the "Course overview" "block" Then I should not see "Category 1" in the "Course overview" "block" @@ -308,12 +308,12 @@ Feature: The my overview block allows users to easily access their courses Scenario: Show course category in summary display Given the following config values are set as admin: | displaycategories | 0 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "Display drop-down menu" "button" in the "Course overview" "block" When I click on "Summary" "link" in the "Course overview" "block" Then I should not see "Category 1" in the "Course overview" "block" @accessibility Scenario: The dashboard page must have sufficient colour contrast - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" Then the page should meet "wcag143" accessibility standards diff --git a/blocks/myoverview/tests/behat/block_myoverview_favourite.feature b/blocks/myoverview/tests/behat/block_myoverview_favourite.feature index fbf575663c275..71d140ea4ca61 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_favourite.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_favourite.feature @@ -24,7 +24,7 @@ Feature: The my overview block allows users to favourite their courses | student1 | C5 | student | Scenario: Favourite a course on a course card - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" And I reload the page @@ -34,7 +34,7 @@ Feature: The my overview block allows users to favourite their courses And "//div[@class='card dashboard-card' and contains(.,'Course 3')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist Scenario: Star a course and switch display to list - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" And I reload the page @@ -47,7 +47,7 @@ Feature: The my overview block allows users to favourite their courses And "//li[contains(concat(' ', normalize-space(@class), ' '), 'list-group-item') and contains(.,'Course 3')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist Scenario: Star a course and switch display to summary - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element" And I reload the page diff --git a/blocks/myoverview/tests/behat/block_myoverview_hidden.feature b/blocks/myoverview/tests/behat/block_myoverview_hidden.feature index 3c4a12de23b7e..e82f34bdf0d84 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_hidden.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_hidden.feature @@ -24,7 +24,7 @@ Feature: The my overview block allows users to hide their courses | student1 | C5 | student | Scenario: Test hide toggle functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -33,7 +33,7 @@ Feature: The my overview block allows users to hide their courses Then I should not see "Course 2" in the "Course overview" "block" Scenario: Test hide toggle functionality w/ favorites - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -50,7 +50,7 @@ Feature: The my overview block allows users to hide their courses Then I should see "Course 2" in the "Course overview" "block" Scenario: Test show toggle functionality - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -65,7 +65,7 @@ Feature: The my overview block allows users to hide their courses Then I should see "Course 2" in the "Course overview" "block" Scenario: Test show toggle functionality w/ favorites - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -87,7 +87,7 @@ Feature: The my overview block allows users to hide their courses Then I should see "Course 2" in the "Course overview" "block" Scenario: Test a course is hidden directly with "All (except removed from view)" courses - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element" @@ -98,7 +98,7 @@ Feature: The my overview block allows users to hide their courses Given the following config values are set as admin: | config | value | plugin | | displaygroupingallincludinghidden | 1 | block_myoverview | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except hidden)" element instead When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature b/blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature index fef9a8b936184..1822a2d7ab9cc 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature @@ -37,7 +37,7 @@ Feature: The my overview block allows users to persistence of their page limits | student1 | C13 | student | Scenario: Toggle the page limit between page reloads - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block" And I click on "All" "link" in the ".dropdown-menu.show" "css_element" Then I should see "Course 13" @@ -46,7 +46,7 @@ Feature: The my overview block allows users to persistence of their page limits And I should see "All" in the "[data-action='limit-toggle']" "css_element" Scenario: Toggle the page limit between grouping changes - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block" And I click on "All" "link" in the ".dropdown-menu.show" "css_element" And I click on "All (except removed from view)" "button" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature index b3a6b514f478e..05f5ea63fe4c4 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature @@ -34,7 +34,7 @@ Feature: My overview block pagination | Course 25 | C25 | 0 | Scenario: The pagination controls should be hidden if I am not enrolled in any courses - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" Then I should see "No courses" in the "Course overview" "block" And I should not see "Show" in the "Course overview" "block" And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible @@ -57,7 +57,7 @@ Feature: My overview block pagination | student1 | C10 | student | | student1 | C11 | student | | student1 | C12 | student | - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" Then I should not see "Show" in the "Course overview" "block" And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible And ".block_myoverview [data-control='next']" "css_element" should not be visible @@ -80,7 +80,7 @@ Feature: My overview block pagination | student1 | C11 | student | | student1 | C12 | student | | student1 | C13 | student | - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" Then I should see "12" in the "[data-action='limit-toggle']" "css_element" And I log out @@ -100,7 +100,7 @@ Feature: My overview block pagination | student1 | C11 | student | | student1 | C12 | student | | student1 | C13 | student | - And I log in as "student1" + And I am on the "My courses" page logged in as "student1" When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block" Then I should see "All" in the ".dropdown-menu.show" "css_element" And I should see "12" in the ".dropdown-menu.show" "css_element" @@ -125,7 +125,7 @@ Feature: My overview block pagination | student1 | C11 | student | | student1 | C12 | student | | student1 | C13 | student | - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" Then the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should contain "disabled" And I log out @@ -145,7 +145,7 @@ Feature: My overview block pagination | student1 | C11 | student | | student1 | C12 | student | | student1 | C13 | student | - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" And I click on "[data-control='next']" "css_element" in the "Course overview" "block" Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled" And I log out @@ -178,7 +178,8 @@ Feature: My overview block pagination | student1 | C23 | student | | student1 | C24 | student | | student1 | C25 | student | - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" + And I wait until ".block_myoverview [data-control='next']" "css_element" exists And I click on "[data-control='next']" "css_element" in the "Course overview" "block" Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should not contain "disabled" And the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should not contain "disabled" diff --git a/blocks/myoverview/tests/behat/block_myoverview_progress.feature b/blocks/myoverview/tests/behat/block_myoverview_progress.feature index 30f133477d110..3d503a2ba07aa 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_progress.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_progress.feature @@ -21,7 +21,7 @@ Feature: Course overview block show users their progress on courses | student1 | C1 | student | Scenario: Course progress percentage should not be displayed if completion is not enabled - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" When I click on "All (except removed from view)" "link" in the "Course overview" "block" Then I should not see "0%" in the "Course overview" "block" @@ -34,12 +34,11 @@ Feature: Course overview block show users their progress on courses | id_completionview | 1 | And I press "Save and return to course" And I log out - When I log in as "student1" + When I am on the "My courses" page logged in as "student1" And I click on "All (except removed from view)" "button" in the "Course overview" "block" Then I should see "Course 1" in the "Course overview" "block" And I should see "0%" in the "Course overview" "block" - And I am on the "Test choice 1" "choice activity" page - And I follow "Dashboard" in the user menu + And I am on the "My courses" page And I click on "All (except removed from view)" "button" in the "Course overview" "block" And I should see "100%" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/behat/block_myoverview_search.feature b/blocks/myoverview/tests/behat/block_myoverview_search.feature index 0878b639f4d0f..e968147efd541 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_search.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_search.feature @@ -39,21 +39,21 @@ Feature: My overview block searching | student1 | C13 | student | Scenario: The search should return no courses if I am not enrolled in any - When I log in as "student2" + When I am on the "My courses" page logged in as "student1" Then I should see "No courses" in the "Course overview" "block" And I set the field "Search courses" to "Fake example" And I should see "No courses" in the "Course overview" "block" And I log out Scenario: Single page search - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I set the field "Search courses" to "Course 0" Then I should see "Course 01" in the "Course overview" "block" And I should not see "Course 13" in the "Course overview" "block" And I log out Scenario: Paginated search - Given I log in as "student1" + Given I am on the "My courses" page logged in as "student1" And I set the field "Search courses" to "Course" And I should see "Course 01" in the "Course overview" "block" And I should not see "Course 13" in the "Course overview" "block" diff --git a/blocks/myoverview/tests/myoverview_test.php b/blocks/myoverview/tests/myoverview_test.php index eba4b29bc4adf..13f130cfc6f46 100644 --- a/blocks/myoverview/tests/myoverview_test.php +++ b/blocks/myoverview/tests/myoverview_test.php @@ -66,15 +66,15 @@ public function test_get_block_config_for_external() { $this->setUser($user); $context = context_user::instance($user->id); - if (!$currentpage = my_get_page($user->id, MY_PAGE_PRIVATE)) { + if (!$currentpage = my_get_page($user->id, MY_PAGE_PUBLIC, MY_PAGE_COURSES)) { throw new moodle_exception('mymoodlesetup'); } - $PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set. + $PAGE->set_url('/my/courses.php'); // Need this because some internal API calls require the $PAGE url to be set. $PAGE->set_context($context); $PAGE->set_pagelayout('mydashboard'); $PAGE->set_pagetype('my-index'); - $PAGE->blocks->add_region('content'); // Need to add this special regition to retrieve the central blocks. + $PAGE->blocks->add_region('content'); // Need to add this special region to retrieve the central blocks. $PAGE->set_subpage($currentpage->id); // Load the block instances for all the regions. diff --git a/blocks/myoverview/version.php b/blocks/myoverview/version.php index 988fee790f497..b8ed58d669a26 100644 --- a/blocks/myoverview/version.php +++ b/blocks/myoverview/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2021052503; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2021052504; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2021052500; // Requires this Moodle version. $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics). From 6d26ba77f3abd634c0f6ff47d67e0c54d774ef87 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Thu, 4 Nov 2021 15:45:14 +0800 Subject: [PATCH 11/57] MDL-72096 core: Add safe ORDER BY helpers for db sorting from user input The new get_safe_orderby() and get_safe_orderby_multiple() methods provide a centralised safe way for user submitted sorting values to be incorporated into SQL ORDER BY. They do this by removing the need for user submitted data to pass in any SQL and not allowing arbitrary column values, instead using string keys which map to a predefined list of allowed sortable columns. --- lib/datalib.php | 94 +++++++++++++++++ lib/tests/datalib_test.php | 201 +++++++++++++++++++++++++++++++++++++ lib/upgrade.txt | 4 + 3 files changed, 299 insertions(+) diff --git a/lib/datalib.php b/lib/datalib.php index 81adff36a5e9b..b8f61119a42f2 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -1852,3 +1852,97 @@ function get_max_courses_in_category() { return $CFG->maxcoursesincategory; } } + +/** + * Prepare a safe ORDER BY statement from user interactable requests. + * + * This allows safe user specified sorting (ORDER BY), by abstracting the SQL from the value being requested by the user. + * A standard string (and optional direction) can be specified, which will be mapped to a predefined allow list of SQL ordering. + * The mapping can optionally include a 'default', which will be used if the key provided is invalid. + * + * Example usage: + * -If $orderbymap = [ + * 'courseid' => 'c.id', + * 'somecustomvalue'=> 'c.startdate, c.shortname', + * 'default' => 'c.fullname', + * ] + * -A value from the map array's keys can be passed in by a user interaction (eg web service) along with an optional direction. + * -get_safe_orderby($orderbymap, 'courseid', 'DESC') would return: ORDER BY c.id DESC + * -get_safe_orderby($orderbymap, 'somecustomvalue') would return: ORDER BY c.startdate, c.shortname + * -get_safe_orderby($orderbymap, 'invalidblah', 'DESC') would return: ORDER BY c.fullname DESC + * -If no default key was specified in $orderbymap, the invalidblah example above would return empty string. + * + * @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'. + * @param string $orderbykey A string to be mapped to a key in $orderbymap. + * @param string $direction Optional ORDER BY direction (ASC/DESC, case insensitive). + * @param bool $useprefix Whether ORDER BY is prefixed to the output (true by default). This should not be modified in most cases. + * It is included to enable get_safe_orderby_multiple() to use this function multiple times. + * @return string The ORDER BY statement, or empty string if $orderbykey is invalid and no default is mapped. + */ +function get_safe_orderby(array $orderbymap, string $orderbykey, string $direction = '', bool $useprefix = true): string { + $orderby = $useprefix ? ' ORDER BY ' : ''; + $output = ''; + + // Only include an order direction if ASC/DESC is explicitly specified (case insensitive). + $direction = strtoupper($direction); + if (!in_array($direction, ['ASC', 'DESC'], true)) { + $direction = ''; + } else { + $direction = " {$direction}"; + } + + // Prepare the statement if the key maps to a defined sort parameter. + if (isset($orderbymap[$orderbykey])) { + $output = "{$orderby}{$orderbymap[$orderbykey]}{$direction}"; + } else if (array_key_exists('default', $orderbymap)) { + // Fall back to use the default if one is specified. + $output = "{$orderby}{$orderbymap['default']}{$direction}"; + } + + return $output; +} + +/** + * Prepare a safe ORDER BY statement from user interactable requests using multiple values. + * + * This allows safe user specified sorting (ORDER BY) similar to get_safe_orderby(), but supports multiple keys and directions. + * This is useful in cases where combinations of columns are needed and/or each item requires a specified direction (ASC/DESC). + * The mapping can optionally include a 'default', which will be used if the key provided is invalid. + * + * Example usage: + * -If $orderbymap = [ + * 'courseid' => 'c.id', + * 'fullname'=> 'c.fullname', + * 'default' => 'c.startdate', + * ] + * -An array of values from the map's keys can be passed in by a user interaction (eg web service), with optional directions. + * -get_safe_orderby($orderbymap, ['courseid', 'fullname'], ['DESC', 'ASC']) would return: ORDER BY c.id DESC, c.fullname ASC + * -get_safe_orderby($orderbymap, ['courseid', 'invalidblah'], ['aaa', 'DESC']) would return: ORDER BY c.id, c.startdate DESC + * -If no default key was specified in $orderbymap, the invalidblah example above would return: ORDER BY c.id + * + * @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'. + * @param array $orderbykeys An array of strings to be mapped to keys in $orderbymap. + * @param array $directions Optional array of ORDER BY direction (ASC/DESC, case insensitive). + * The array keys should match array keys in $orderbykeys. + * @return string The ORDER BY statement, or empty string if $orderbykeys contains no valid items and no default is mapped. + */ +function get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions = []): string { + $output = ''; + + // Check each key for a valid mapping and add to the ORDER BY statement (invalid entries will be empty strings). + foreach ($orderbykeys as $index => $orderbykey) { + $direction = $directions[$index] ?? ''; + $safeorderby = get_safe_orderby($orderbymap, $orderbykey, $direction, false); + + if (!empty($safeorderby)) { + $output .= ", {$safeorderby}"; + } + } + + // Prefix with ORDER BY if any valid ordering is specified (and remove comma from the start). + if (!empty($output)) { + $output = ' ORDER BY' . ltrim($output, ','); + } + + return $output; +} diff --git a/lib/tests/datalib_test.php b/lib/tests/datalib_test.php index b81ddefdab639..8891a62f73f3e 100644 --- a/lib/tests/datalib_test.php +++ b/lib/tests/datalib_test.php @@ -894,4 +894,205 @@ public function test_get_users_listing(): void { $results = array_diff_key($results, $existingids); $this->assertEquals([$userids[1], $userids[3]], array_keys($results)); } + + /** + * Data provider for test_get_safe_orderby(). + * + * @return array + */ + public function get_safe_orderby_provider(): array { + $orderbymap = [ + 'courseid' => 'c.id', + 'somecustomvalue' => 'c.startdate, c.shortname', + 'default' => 'c.fullname', + ]; + $orderbymapnodefault = [ + 'courseid' => 'c.id', + 'somecustomvalue' => 'c.startdate, c.shortname', + ]; + + return [ + 'Valid option, no direction specified' => [ + $orderbymap, + 'somecustomvalue', + '', + ' ORDER BY c.startdate, c.shortname', + ], + 'Valid option, valid direction specified' => [ + $orderbymap, + 'courseid', + 'DESC', + ' ORDER BY c.id DESC', + ], + 'Valid option, valid lowercase direction specified' => [ + $orderbymap, + 'courseid', + 'asc', + ' ORDER BY c.id ASC', + ], + 'Valid option, invalid direction specified' => [ + $orderbymap, + 'courseid', + 'BOOP', + ' ORDER BY c.id', + ], + 'Valid option, invalid lowercase direction specified' => [ + $orderbymap, + 'courseid', + 'boop', + ' ORDER BY c.id', + ], + 'Invalid option default fallback, with valid direction' => [ + $orderbymap, + 'thisdoesnotexist', + 'ASC', + ' ORDER BY c.fullname ASC', + ], + 'Invalid option default fallback, with invalid direction' => [ + $orderbymap, + 'thisdoesnotexist', + 'BOOP', + ' ORDER BY c.fullname', + ], + 'Invalid option without default, with valid direction' => [ + $orderbymapnodefault, + 'thisdoesnotexist', + 'ASC', + '', + ], + 'Invalid option without default, with invalid direction' => [ + $orderbymapnodefault, + 'thisdoesnotexist', + 'NOPE', + '', + ], + ]; + } + + /** + * Tests the get_safe_orderby function. + * + * @dataProvider get_safe_orderby_provider + * @param array $orderbymap The ORDER BY parameter mapping array. + * @param string $orderbykey The string key being provided, to check against the map. + * @param string $direction The optional direction to order by. + * @param string $expected The expected string output of the method. + */ + public function test_get_safe_orderby(array $orderbymap, string $orderbykey, string $direction, string $expected): void { + $actual = get_safe_orderby($orderbymap, $orderbykey, $direction); + $this->assertEquals($expected, $actual); + } + + /** + * Data provider for test_get_safe_orderby_multiple(). + * + * @return array + */ + public function get_safe_orderby_multiple_provider(): array { + $orderbymap = [ + 'courseid' => 'c.id', + 'firstname' => 'u.firstname', + 'default' => 'c.startdate', + ]; + $orderbymapnodefault = [ + 'courseid' => 'c.id', + 'firstname' => 'u.firstname', + ]; + + return [ + 'Valid options, no directions specified' => [ + $orderbymap, + ['courseid', 'firstname'], + [], + ' ORDER BY c.id, u.firstname', + ], + 'Valid options, some direction specified' => [ + $orderbymap, + ['courseid', 'firstname'], + ['DESC'], + ' ORDER BY c.id DESC, u.firstname', + ], + 'Valid options, all directions specified' => [ + $orderbymap, + ['courseid', 'firstname'], + ['ASC', 'desc'], + ' ORDER BY c.id ASC, u.firstname DESC', + ], + 'Valid options, valid and invalid directions specified' => [ + $orderbymap, + ['courseid', 'firstname'], + ['BOOP', 'DESC'], + ' ORDER BY c.id, u.firstname DESC', + ], + 'Valid options, all invalid directions specified' => [ + $orderbymap, + ['courseid', 'firstname'], + ['BOOP', 'SNOOT'], + ' ORDER BY c.id, u.firstname', + ], + 'Valid and invalid option default fallback, with valid directions' => [ + $orderbymap, + ['thisdoesnotexist', 'courseid'], + ['asc', 'DESC'], + ' ORDER BY c.startdate ASC, c.id DESC', + ], + 'Valid and invalid option default fallback, with invalid direction' => [ + $orderbymap, + ['courseid', 'thisdoesnotexist'], + ['BOOP', 'SNOOT'], + ' ORDER BY c.id, c.startdate', + ], + 'Valid and invalid option without default, with valid direction' => [ + $orderbymapnodefault, + ['thisdoesnotexist', 'courseid'], + ['ASC', 'DESC'], + ' ORDER BY c.id DESC', + ], + 'Valid and invalid option without default, with invalid direction' => [ + $orderbymapnodefault, + ['thisdoesnotexist', 'courseid'], + ['BOOP', 'SNOOT'], + ' ORDER BY c.id', + ], + 'Invalid option only without default, with valid direction' => [ + $orderbymapnodefault, + ['thisdoesnotexist'], + ['ASC'], + '', + ], + 'Invalid option only without default, with invalid direction' => [ + $orderbymapnodefault, + ['thisdoesnotexist'], + ['BOOP'], + '', + ], + 'Single valid option, direction specified' => [ + $orderbymap, + ['firstname'], + ['ASC'], + ' ORDER BY u.firstname ASC', + ], + 'Single valid option, direction not specified' => [ + $orderbymap, + ['firstname'], + [], + ' ORDER BY u.firstname', + ], + ]; + } + + /** + * Tests the get_safe_orderby_multiple function. + * + * @dataProvider get_safe_orderby_multiple_provider + * @param array $orderbymap The ORDER BY parameter mapping array. + * @param array $orderbykeys The array of string keys being provided, to check against the map. + * @param array $directions The optional directions to order by. + * @param string $expected The expected string output of the method. + */ + public function test_get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions, + string $expected): void { + $actual = get_safe_orderby_multiple($orderbymap, $orderbykeys, $directions); + $this->assertEquals($expected, $actual); + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 6d512974871d2..5ee1a2806942c 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -123,6 +123,10 @@ completely removed from Moodle core too. * New html_table attribute "$responsive" which defaults to true. When set to true, tables created via html_writer::table() will be enclosed in a .table-responsive div container which will allow the table to be scrolled horizontally with ease, especially when the table is rendered in smaller viewports. Set to false to prevent the table from being enclosed in the responsive container. +* Two new helper functions have been added to lib/datalib.php, for safely preparing SQL ORDER BY statements where user + interactions define sort parameters (see the respective docblocks for full details and examples): + -get_safe_orderby() - where a single sort parameter is required. + -get_safe_orderby_multiple() - where multiple sort parameters are required. === 3.11.4 === * A new option dontforcesvgdownload has been added to the $options parameter of the send_file() function. From 4f96d7ac924bef4c92f536cbeae3690b89d49acb Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 5 Aug 2021 17:15:49 +1000 Subject: [PATCH 12/57] MDL-70721 core: a new mustache helper to clean string after get_string --- lib/amd/build/templates.min.js | 2 +- lib/amd/build/templates.min.js.map | 2 +- lib/amd/src/templates.js | 36 ++++++++-- .../output/mustache_clean_string_helper.php | 68 +++++++++++++++++++ lib/outputrenderers.php | 2 + .../mustache_clean_string_helper_test.php | 56 +++++++++++++++ lib/upgrade.txt | 1 + 7 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 lib/classes/output/mustache_clean_string_helper.php create mode 100644 lib/tests/output/mustache_clean_string_helper_test.php diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js index 36b4c671519a5..23d541ec828ad 100644 --- a/lib/amd/build/templates.min.js +++ b/lib/amd/build/templates.min.js @@ -1,2 +1,2 @@ -define ("core/templates",["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/config","core/localstorage","core/icon_system","core_filters/events","core/yui","core/log","core/truncate","core/user_date","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p=0,q={},r={},s={},t={},u=[],v=!1,w=["js"],x=function(a){if(a){if("moodle"!==a&&"core"!==a){return a}}return"core"},y=function(a){if(a in r){return r[a]}if(a in q){r[a]=b.Deferred().resolve(q[a]).promise();return r[a]}if(0>=M.cfg.templaterev){return null}var c=h.get("core_template/"+M.cfg.templaterev+":"+a);if(c){q[a]=c;r[a]=b.Deferred().resolve(c).promise();return r[a]}return null},z=function(){if(!u.length){return}if(v){return}v=!0;var a=u.slice(),e=b.Deferred(),f=[],g=a.map(function(a){var c=x(a.component),i=a.name,j=a.searchKey,k=a.theme,l=a.deferred,m=null,n=y(j);if(n){m=n}else{f.push({methodname:"core_output_load_template_with_dependencies",args:{component:c,template:i,themename:k,lang:b("html").attr("lang").replace(/-/g,"_")}});var o=f.length-1;m=e.promise().then(function(a){g[j]=a[o].then(function(a){var b=null;a.templates.forEach(function(a){a.component=x(a.component);var d=[k,a.component,a.name].join("/");q[d]=a.value;if(0=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g," ");return"\""+d+"\""};A.prototype.shortenTextHelper=function(a,b,c){var d=b.match(/(.*?),(.*)/),e=d[1].trim(),f=d[2].trim(),g=c(f,a);return m.truncate(g,{length:e,words:!0,ellipsis:"..."})};A.prototype.userDateHelper=function(a,b,c){var d=b.match(/(.*?),(.*)/),e=c(d[1].trim(),a),f=c(d[2].trim(),a),g=this.requiredDates.length;this.requiredDates.push({timestamp:e,format:f});return"[[_t_"+g+"]]"};A.prototype.addHelperFunction=function(a,b){return function(){return function(c,d){var e=w.reduce(function(a,c){if(b.hasOwnProperty(c)){a[c]=b[c]}return a},{});w.forEach(function(a){b[a]=function(){return""}});var f=a.apply(this,[b,c,d]);for(var g in e){b[g]=e[g]}return f}.bind(this)}.bind(this)};A.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=this.addHelperFunction(this.stringHelper,a);a.pix=this.addHelperFunction(this.pixHelper,a);a.js=this.addHelperFunction(this.jsHelper,a);a.quote=this.addHelperFunction(this.quoteHelper,a);a.shortentext=this.addHelperFunction(this.shortenTextHelper,a);a.userdate=this.addHelperFunction(this.userDateHelper,a);a.globals={config:g};a.currentTheme=b};A.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},C=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}B(d);j.notifyFilterContentUpdated(g);return g.get()}return[]};A.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4=M.cfg.templaterev){return null}var c=h.get("core_template/"+M.cfg.templaterev+":"+a);if(c){q[a]=c;r[a]=b.Deferred().resolve(c).promise();return r[a]}return null},z=function(){if(!u.length){return}if(v){return}v=!0;var a=u.slice(),e=b.Deferred(),f=[],g=a.map(function(a){var c=x(a.component),i=a.name,j=a.searchKey,k=a.theme,l=a.deferred,m=null,n=y(j);if(n){m=n}else{f.push({methodname:"core_output_load_template_with_dependencies",args:{component:c,template:i,themename:k,lang:b("html").attr("lang").replace(/-/g,"_")}});var o=f.length-1;m=e.promise().then(function(a){g[j]=a[o].then(function(a){var b=null;a.templates.forEach(function(a){a.component=x(a.component);var d=[k,a.component,a.name].join("/");q[d]=a.value;if(0=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g," ");return"\""+d+"\""};A.prototype.shortenTextHelper=function(a,b,c){var d=b.match(/(.*?),(.*)/),e=d[1].trim(),f=d[2].trim(),g=c(f,a);return m.truncate(g,{length:e,words:!0,ellipsis:"..."})};A.prototype.userDateHelper=function(a,b,c){var d=b.match(/(.*?),(.*)/),e=c(d[1].trim(),a),f=c(d[2].trim(),a),g=this.requiredDates.length;this.requiredDates.push({timestamp:e,format:f});return"[[_t_"+g+"]]"};A.prototype.addHelperFunction=function(a,b){return function(){return function(c,d){var e=w.reduce(function(a,c){if(b.hasOwnProperty(c)){a[c]=b[c]}return a},{});w.forEach(function(a){b[a]=function(){return""}});var f=a.apply(this,[b,c,d]);for(var g in e){b[g]=e[g]}return f}.bind(this)}.bind(this)};A.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=this.addHelperFunction(this.stringHelper,a);a.cleanstr=this.addHelperFunction(this.cleanStringHelper,a);a.pix=this.addHelperFunction(this.pixHelper,a);a.js=this.addHelperFunction(this.jsHelper,a);a.quote=this.addHelperFunction(this.quoteHelper,a);a.shortentext=this.addHelperFunction(this.shortenTextHelper,a);a.userdate=this.addHelperFunction(this.userDateHelper,a);a.globals={config:g};a.currentTheme=b};A.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},C=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}B(d);j.notifyFilterContentUpdated(g);return g.get()}return[]};A.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4.\n\n/**\n * Template renderer for Moodle. Load and render Moodle templates with Mustache.\n *\n * @module core/templates\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 2.9\n */\ndefine([\n 'core/mustache',\n 'jquery',\n 'core/ajax',\n 'core/str',\n 'core/notification',\n 'core/url',\n 'core/config',\n 'core/localstorage',\n 'core/icon_system',\n 'core_filters/events',\n 'core/yui',\n 'core/log',\n 'core/truncate',\n 'core/user_date',\n 'core/pending',\n],\nfunction(\n mustache,\n $,\n ajax,\n str,\n notification,\n coreurl,\n config,\n storage,\n IconSystem,\n filterEvents,\n Y,\n Log,\n Truncate,\n UserDate,\n Pending\n) {\n\n // Module variables.\n /** @var {Number} uniqInstances Count of times this constructor has been called. */\n var uniqInstances = 0;\n\n /** @var {String[]} templateCache - Cache of already loaded template strings */\n var templateCache = {};\n\n /** @var {Promise[]} templatePromises - Cache of already loaded template promises */\n var templatePromises = {};\n\n /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */\n var cachePartialPromises = {};\n\n /** @var {Object} iconSystem - Object extending core/iconsystem */\n var iconSystem = {};\n\n /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */\n var loadTemplateBuffer = [];\n\n /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */\n var isLoadingTemplates = false;\n\n /** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers */\n var disallowedNestedHelpers = ['js'];\n\n /**\n * Normalise the provided component such that '', 'moodle', and 'core' are treated consistently.\n *\n * @param {String} component\n * @returns {String}\n */\n var getNormalisedComponent = function(component) {\n if (component) {\n if (component !== 'moodle' && component !== 'core') {\n return component;\n }\n }\n\n return 'core';\n };\n\n /**\n * Search the various caches for a template promise for the given search key.\n * The search key should be in the format //