From 1b339cd6d86d059d80d01a569ecd75416bc7efaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Fri, 25 Jul 2025 17:58:57 +0200 Subject: [PATCH] MDL-86036 course: Add gradable info to activity chooser --- .upgradenotes/MDL-86036-2025081409210428.yml | 5 ++ .../local/activitychooser/exporter.min.js | 2 +- .../local/activitychooser/exporter.min.js.map | 2 +- .../amd/src/local/activitychooser/exporter.js | 7 ++ .../classes/local/entity/content_item.php | 76 +++++++++---------- .../course_content_item_exporter.php | 2 + .../content_item_readonly_repository.php | 44 ++++++----- .../tests/behat/activity_chooser.feature | 11 +++ public/course/tests/content_item_test.php | 17 ++++- public/lang/en/moodle.php | 1 + 10 files changed, 100 insertions(+), 67 deletions(-) create mode 100644 .upgradenotes/MDL-86036-2025081409210428.yml diff --git a/.upgradenotes/MDL-86036-2025081409210428.yml b/.upgradenotes/MDL-86036-2025081409210428.yml new file mode 100644 index 00000000000..2e56f498e8b --- /dev/null +++ b/.upgradenotes/MDL-86036-2025081409210428.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-86036 +notes: + core_course: + - message: Added new `gradable` property to `core_course\local\entity\content_item` + type: improved diff --git a/public/course/amd/build/local/activitychooser/exporter.min.js b/public/course/amd/build/local/activitychooser/exporter.min.js index 426c69df7ce..72868cc0ecc 100644 --- a/public/course/amd/build/local/activitychooser/exporter.min.js +++ b/public/course/amd/build/local/activitychooser/exporter.min.js @@ -6,6 +6,6 @@ define("core_course/local/activitychooser/exporter",["exports","core/str"],(func * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const activityCategories=["administration","assessment","collaboration","communication","content","interactivecontent"];let allStrings=null;loadNecessaryStrings();async function loadNecessaryStrings(){if(null!==allStrings)return allStrings;allStrings={};const stringToLoad=[{key:"all",component:"core"},{key:"favourites",component:"core"},{key:"recommended",component:"core"},{key:"recommended_help",component:"core_course"},{key:"supports",component:"core_course"},...activityCategories.map((key=>({key:"mod_purpose_"+key,component:"core_course"}))),...activityCategories.map((key=>({key:"mod_purpose_"+key+"_help",component:"core_course"})))],loadedStrings=await(0,_str.getStrings)(stringToLoad);return stringToLoad.forEach(((_ref,index)=>{let{key:key}=_ref;allStrings[key]=loadedStrings[index]})),allStrings}return _exports.default=class{getTabData(tabId,filteredModules,tabLabel){let tabHelp=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const result={tabId:tabId,active:arguments.length>4&&void 0!==arguments[4]&&arguments[4],items:filteredModules,displayed:filteredModules.length>0,tabLabel:tabLabel};return tabHelp&&(result.tabHelp=tabHelp),result}normaliseModulesData(modulesData){if(modulesData instanceof Map)modulesData=Array.from(modulesData.values());else if(!Array.isArray(modulesData))throw new Error("Invalid modules data format. Expected an array or a Map.");return modulesData}async getModChooserTemplateData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteTab=await this.getFavouriteTabData(modulesData),tabs=[{...this.getTabData("all",modulesData,allStrings.all,null,!favouriteTab.displayed),hasSearchResults:!0},favouriteTab,{...this.getTabData("recommended",modulesData.filter((mod=>!0===mod.recommended)),allStrings.recommended,allStrings.recommended_help),separator:!0}];return activityCategories.forEach((category=>{const categoryModules=modulesData.filter((mod=>mod.purpose==category));0!==categoryModules.length&&tabs.push(this.getTabData(category,categoryModules,allStrings["mod_purpose_"+category],allStrings["mod_purpose_"+category+"_help"]))})),{modules:modulesData,tabs:tabs}}async getModuleHelpTemplateData(moduleData){const purposeKey="mod_purpose_".concat(moduleData.purpose),allStrings=await loadNecessaryStrings(),data={...moduleData};return data.details=[{label:allStrings.supports,value:allStrings[purposeKey]||""}],data}async getFavouriteTabData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteModules=modulesData.filter((mod=>!0===mod.favourite));return this.getTabData("favourites",favouriteModules,allStrings.favourites,null,favouriteModules.length>0)}getSearchResultData(resultsModulesData){return{searchresultsnumber:(resultsModulesData=this.normaliseModulesData(resultsModulesData)).length,searchresults:resultsModulesData}}countTabItems(tabData){var _tabData$items$length,_tabData$items;return null!==(_tabData$items$length=null===(_tabData$items=tabData.items)||void 0===_tabData$items?void 0:_tabData$items.length)&&void 0!==_tabData$items$length?_tabData$items$length:0}getFooterData(footerData){return{activeFooter:footerData.customfootertemplate}}},_exports.default})); +const activityCategories=["administration","assessment","collaboration","communication","content","interactivecontent"];let allStrings=null;loadNecessaryStrings();async function loadNecessaryStrings(){if(null!==allStrings)return allStrings;allStrings={};const stringToLoad=[{key:"all",component:"core"},{key:"yes",component:"core"},{key:"no",component:"core"},{key:"favourites",component:"core"},{key:"recommended",component:"core"},{key:"gradable",component:"core"},{key:"recommended_help",component:"core_course"},{key:"supports",component:"core_course"},...activityCategories.map((key=>({key:"mod_purpose_"+key,component:"core_course"}))),...activityCategories.map((key=>({key:"mod_purpose_"+key+"_help",component:"core_course"})))],loadedStrings=await(0,_str.getStrings)(stringToLoad);return stringToLoad.forEach(((_ref,index)=>{let{key:key}=_ref;allStrings[key]=loadedStrings[index]})),allStrings}return _exports.default=class{getTabData(tabId,filteredModules,tabLabel){let tabHelp=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const result={tabId:tabId,active:arguments.length>4&&void 0!==arguments[4]&&arguments[4],items:filteredModules,displayed:filteredModules.length>0,tabLabel:tabLabel};return tabHelp&&(result.tabHelp=tabHelp),result}normaliseModulesData(modulesData){if(modulesData instanceof Map)modulesData=Array.from(modulesData.values());else if(!Array.isArray(modulesData))throw new Error("Invalid modules data format. Expected an array or a Map.");return modulesData}async getModChooserTemplateData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteTab=await this.getFavouriteTabData(modulesData),tabs=[{...this.getTabData("all",modulesData,allStrings.all,null,!favouriteTab.displayed),hasSearchResults:!0},favouriteTab,{...this.getTabData("recommended",modulesData.filter((mod=>!0===mod.recommended)),allStrings.recommended,allStrings.recommended_help),separator:!0}];return activityCategories.forEach((category=>{const categoryModules=modulesData.filter((mod=>mod.purpose==category));0!==categoryModules.length&&tabs.push(this.getTabData(category,categoryModules,allStrings["mod_purpose_"+category],allStrings["mod_purpose_"+category+"_help"]))})),{modules:modulesData,tabs:tabs}}async getModuleHelpTemplateData(moduleData){const purposeKey="mod_purpose_".concat(moduleData.purpose),allStrings=await loadNecessaryStrings(),data={...moduleData};return data.details=[{label:allStrings.supports,value:allStrings[purposeKey]||""},{label:allStrings.gradable,value:moduleData.gradable?allStrings.yes:allStrings.no}],data}async getFavouriteTabData(modulesData){modulesData=this.normaliseModulesData(modulesData);const allStrings=await loadNecessaryStrings(),favouriteModules=modulesData.filter((mod=>!0===mod.favourite));return this.getTabData("favourites",favouriteModules,allStrings.favourites,null,favouriteModules.length>0)}getSearchResultData(resultsModulesData){return{searchresultsnumber:(resultsModulesData=this.normaliseModulesData(resultsModulesData)).length,searchresults:resultsModulesData}}countTabItems(tabData){var _tabData$items$length,_tabData$items;return null!==(_tabData$items$length=null===(_tabData$items=tabData.items)||void 0===_tabData$items?void 0:_tabData$items.length)&&void 0!==_tabData$items$length?_tabData$items$length:0}getFooterData(footerData){return{activeFooter:footerData.customfootertemplate}}},_exports.default})); //# sourceMappingURL=exporter.min.js.map \ No newline at end of file diff --git a/public/course/amd/build/local/activitychooser/exporter.min.js.map b/public/course/amd/build/local/activitychooser/exporter.min.js.map index 1592eeada0c..8a87bf470e6 100644 --- a/public/course/amd/build/local/activitychooser/exporter.min.js.map +++ b/public/course/amd/build/local/activitychooser/exporter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"exporter.min.js","sources":["../../../src/local/activitychooser/exporter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to generate template data for the activity chooser.\n *\n * @module core_course/local/activitychooser/exporter\n * @copyright 2025 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getStrings} from 'core/str';\n\nconst activityCategories = [\n 'administration',\n 'assessment',\n 'collaboration',\n 'communication',\n 'content',\n 'interactivecontent',\n];\n\nlet allStrings = null;\n\nloadNecessaryStrings();\n\nexport default class {\n /**\n * A tab data structure.\n *\n * @typedef {object} TabData\n * @property {String} tabId the tab ID\n * @property {Boolean} active whether the tab is active or not\n * @property {Array} items the filtered modules to be displayed in the tab\n * @property {Boolean} displayed whether the tab is displayed or not\n * @property {String} tabLabel the tab label\n * @property {String|null} tabHelp the help text for the tab (optional)\n */\n\n /**\n * Generate a tab data object for the activity chooser.\n *\n * @private\n * @param {String} tabId Tab ID.\n * @param {Array} filteredModules Filtered modules to be displayed in the tab.\n * @param {String} tabLabel Tab label.\n * @param {String|null} tabHelp Help text for the tab (optional).\n * @param {Boolean} active Whether the tab is active or not.\n * @return {TabData} Tab data object.\n */\n getTabData(tabId, filteredModules, tabLabel, tabHelp = null, active = false) {\n const result = {\n tabId: tabId,\n active: active,\n items: filteredModules,\n displayed: filteredModules.length > 0,\n tabLabel,\n };\n if (tabHelp) {\n result.tabHelp = tabHelp;\n }\n return result;\n }\n\n /**\n * Normalise the modules data to be used in the chooser.\n *\n * The modulesData can be a plain array or a Map. This method will convert it to a\n * plain array of objects.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Array} Normalised modules data.\n */\n normaliseModulesData(modulesData) {\n if (modulesData instanceof Map) {\n modulesData = Array.from(modulesData.values());\n } else if (!Array.isArray(modulesData)) {\n throw new Error('Invalid modules data format. Expected an array or a Map.');\n }\n return modulesData;\n }\n\n /**\n * Fetch the chooser template data for a specific section.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getModChooserTemplateData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n const favouriteTab = await this.getFavouriteTabData(modulesData);\n\n const tabs = [\n {\n ...this.getTabData(\n 'all',\n modulesData,\n allStrings.all,\n null,\n !favouriteTab.displayed,\n ),\n hasSearchResults: true, // The all tab will also show search results.\n },\n favouriteTab,\n {\n ...this.getTabData(\n 'recommended',\n modulesData.filter(mod => mod.recommended === true),\n allStrings.recommended,\n allStrings.recommended_help\n ),\n separator: true, // Add a separator before the purpose categories.\n },\n ];\n\n activityCategories.forEach((category) => {\n const categoryModules = modulesData.filter(mod => mod.purpose == category);\n if (categoryModules.length === 0) {\n return;\n }\n tabs.push(\n this.getTabData(\n category,\n categoryModules,\n allStrings['mod_purpose_' + category],\n allStrings['mod_purpose_' + category + '_help']\n )\n );\n });\n\n return {\n modules: modulesData,\n tabs,\n };\n }\n\n /**\n * Get the module help template data.\n *\n * @param {Object} moduleData Data of the module to get help for.\n * @return {Promise} Promise resolved with the module help data.\n */\n async getModuleHelpTemplateData(moduleData) {\n const purposeKey = `mod_purpose_${moduleData.purpose}`;\n const allStrings = await loadNecessaryStrings();\n const data = {\n ...moduleData,\n };\n data.details = [\n {\n label: allStrings['supports'],\n value: allStrings[purposeKey] || '',\n },\n ];\n return data;\n }\n\n /**\n * Get the favourite tab data.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getFavouriteTabData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n\n // We need to deconstruct the modules data to ensure it is an array.\n const favouriteModules = modulesData.filter(\n mod => {\n return mod.favourite === true;\n }\n );\n\n return this.getTabData(\n 'favourites',\n favouriteModules,\n allStrings.favourites,\n null,\n favouriteModules.length > 0,\n );\n }\n\n /**\n * Get the search result template data.\n *\n * @param {Array|Map} resultsModulesData Modules data to be used in the chooser.\n * @return {Object} The template data.\n */\n getSearchResultData(resultsModulesData) {\n resultsModulesData = this.normaliseModulesData(resultsModulesData);\n return {\n 'searchresultsnumber': resultsModulesData.length,\n 'searchresults': resultsModulesData,\n };\n }\n\n /**\n * Get the number of items in a tab.\n *\n * @param {TabData} tabData The tab data.\n * @return {Number} The number of items in the tab.\n */\n countTabItems(tabData) {\n return tabData.items?.length ?? 0;\n }\n\n /**\n * Get the activity chooser footer template data.\n * @param {Object} footerData The active footer data object.\n * @return {Object} The template data.\n */\n getFooterData(footerData) {\n return {\n 'activeFooter': footerData.customfootertemplate,\n };\n }\n}\n\n/**\n * Load the necessary strings for the activity chooser.\n *\n * @return {Promise} Promise resolved with the loaded strings.\n */\nasync function loadNecessaryStrings() {\n if (allStrings !== null) {\n return allStrings;\n }\n allStrings = {};\n\n const stringToLoad = [\n {key: 'all', component: 'core'},\n {key: 'favourites', component: 'core'},\n {key: 'recommended', component: 'core'},\n {key: 'recommended_help', component: 'core_course'},\n {key: 'supports', component: 'core_course'},\n ...activityCategories.map(\n (key) => ({\n key: 'mod_purpose_' + key,\n component: 'core_course',\n })\n ),\n ...activityCategories.map(\n (key) => ({\n key: 'mod_purpose_' + key + '_help',\n component: 'core_course',\n })\n ),\n ];\n\n const loadedStrings = await getStrings(stringToLoad);\n stringToLoad.forEach(({key}, index) => {\n allStrings[key] = loadedStrings[index];\n });\n return allStrings;\n}\n"],"names":["activityCategories","allStrings","loadNecessaryStrings","stringToLoad","key","component","map","loadedStrings","forEach","index","getTabData","tabId","filteredModules","tabLabel","tabHelp","result","active","items","displayed","length","normaliseModulesData","modulesData","Map","Array","from","values","isArray","Error","this","favouriteTab","getFavouriteTabData","tabs","all","hasSearchResults","filter","mod","recommended","recommended_help","separator","category","categoryModules","purpose","push","modules","moduleData","purposeKey","data","details","label","value","favouriteModules","favourite","favourites","getSearchResultData","resultsModulesData","countTabItems","tabData","_tabData$items","getFooterData","footerData","customfootertemplate"],"mappings":";;;;;;;;MAyBMA,mBAAqB,CACvB,iBACA,aACA,gBACA,gBACA,UACA,0BAGAC,WAAa,KAEjBC,sCAyMeA,0BACQ,OAAfD,kBACOA,WAEXA,WAAa,SAEPE,aAAe,CACjB,CAACC,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,aAAcC,UAAW,QAC/B,CAACD,IAAK,cAAeC,UAAW,QAChC,CAACD,IAAK,mBAAoBC,UAAW,eACrC,CAACD,IAAK,WAAYC,UAAW,kBAC1BL,mBAAmBM,KACjBF,OACGA,IAAK,eAAiBA,IACtBC,UAAW,qBAGhBL,mBAAmBM,KACjBF,OACGA,IAAK,eAAiBA,IAAM,QAC5BC,UAAW,mBAKjBE,oBAAsB,mBAAWJ,qBACvCA,aAAaK,SAAQ,MAAQC,aAAPL,IAACA,UACnBH,WAAWG,KAAOG,cAAcE,UAE7BR,yCA7MPS,WAAWC,MAAOC,gBAAiBC,cAAUC,+DAAU,WAC7CC,OAAS,CACXJ,MAAOA,MACPK,+DACAC,MAAOL,gBACPM,UAAWN,gBAAgBO,OAAS,EACpCN,SAAAA,iBAEAC,UACAC,OAAOD,QAAUA,SAEdC,OAYXK,qBAAqBC,gBACbA,uBAAuBC,IACvBD,YAAcE,MAAMC,KAAKH,YAAYI,eAClC,IAAKF,MAAMG,QAAQL,mBAChB,IAAIM,MAAM,mEAEbN,4CASqBA,aAC5BA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBACnB2B,mBAAqBD,KAAKE,oBAAoBT,aAE9CU,KAAO,CACT,IACOH,KAAKlB,WACJ,MACAW,YACApB,WAAW+B,IACX,MACCH,aAAaX,WAElBe,kBAAkB,GAEtBJ,aACA,IACOD,KAAKlB,WACJ,cACAW,YAAYa,QAAOC,MAA2B,IAApBA,IAAIC,cAC9BnC,WAAWmC,YACXnC,WAAWoC,kBAEfC,WAAW,WAInBtC,mBAAmBQ,SAAS+B,iBAClBC,gBAAkBnB,YAAYa,QAAOC,KAAOA,IAAIM,SAAWF,WAClC,IAA3BC,gBAAgBrB,QAGpBY,KAAKW,KACDd,KAAKlB,WACD6B,SACAC,gBACAvC,WAAW,eAAiBsC,UAC5BtC,WAAW,eAAiBsC,SAAW,cAK5C,CACHI,QAAStB,YACTU,KAAAA,sCAUwBa,kBACtBC,iCAA4BD,WAAWH,SACvCxC,iBAAmBC,uBACnB4C,KAAO,IACNF,mBAEPE,KAAKC,QAAU,CACX,CACIC,MAAO/C,WAAU,SACjBgD,MAAOhD,WAAW4C,aAAe,KAGlCC,+BASezB,aACtBA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBAGnBgD,iBAAmB7B,YAAYa,QACjCC,MAC6B,IAAlBA,IAAIgB,mBAIZvB,KAAKlB,WACR,aACAwC,iBACAjD,WAAWmD,WACX,KACAF,iBAAiB/B,OAAS,GAUlCkC,oBAAoBC,0BAET,sBADPA,mBAAqB1B,KAAKR,qBAAqBkC,qBAEDnC,qBACzBmC,oBAUzBC,cAAcC,8GACHA,QAAQvC,uCAARwC,eAAetC,8DAAU,EAQpCuC,cAAcC,kBACH,cACaA,WAAWC"} \ No newline at end of file +{"version":3,"file":"exporter.min.js","sources":["../../../src/local/activitychooser/exporter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to generate template data for the activity chooser.\n *\n * @module core_course/local/activitychooser/exporter\n * @copyright 2025 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getStrings} from 'core/str';\n\nconst activityCategories = [\n 'administration',\n 'assessment',\n 'collaboration',\n 'communication',\n 'content',\n 'interactivecontent',\n];\n\nlet allStrings = null;\n\nloadNecessaryStrings();\n\nexport default class {\n /**\n * A tab data structure.\n *\n * @typedef {object} TabData\n * @property {String} tabId the tab ID\n * @property {Boolean} active whether the tab is active or not\n * @property {Array} items the filtered modules to be displayed in the tab\n * @property {Boolean} displayed whether the tab is displayed or not\n * @property {String} tabLabel the tab label\n * @property {String|null} tabHelp the help text for the tab (optional)\n */\n\n /**\n * Generate a tab data object for the activity chooser.\n *\n * @private\n * @param {String} tabId Tab ID.\n * @param {Array} filteredModules Filtered modules to be displayed in the tab.\n * @param {String} tabLabel Tab label.\n * @param {String|null} tabHelp Help text for the tab (optional).\n * @param {Boolean} active Whether the tab is active or not.\n * @return {TabData} Tab data object.\n */\n getTabData(tabId, filteredModules, tabLabel, tabHelp = null, active = false) {\n const result = {\n tabId: tabId,\n active: active,\n items: filteredModules,\n displayed: filteredModules.length > 0,\n tabLabel,\n };\n if (tabHelp) {\n result.tabHelp = tabHelp;\n }\n return result;\n }\n\n /**\n * Normalise the modules data to be used in the chooser.\n *\n * The modulesData can be a plain array or a Map. This method will convert it to a\n * plain array of objects.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Array} Normalised modules data.\n */\n normaliseModulesData(modulesData) {\n if (modulesData instanceof Map) {\n modulesData = Array.from(modulesData.values());\n } else if (!Array.isArray(modulesData)) {\n throw new Error('Invalid modules data format. Expected an array or a Map.');\n }\n return modulesData;\n }\n\n /**\n * Fetch the chooser template data for a specific section.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getModChooserTemplateData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n const favouriteTab = await this.getFavouriteTabData(modulesData);\n\n const tabs = [\n {\n ...this.getTabData(\n 'all',\n modulesData,\n allStrings.all,\n null,\n !favouriteTab.displayed,\n ),\n hasSearchResults: true, // The all tab will also show search results.\n },\n favouriteTab,\n {\n ...this.getTabData(\n 'recommended',\n modulesData.filter(mod => mod.recommended === true),\n allStrings.recommended,\n allStrings.recommended_help\n ),\n separator: true, // Add a separator before the purpose categories.\n },\n ];\n\n activityCategories.forEach((category) => {\n const categoryModules = modulesData.filter(mod => mod.purpose == category);\n if (categoryModules.length === 0) {\n return;\n }\n tabs.push(\n this.getTabData(\n category,\n categoryModules,\n allStrings['mod_purpose_' + category],\n allStrings['mod_purpose_' + category + '_help']\n )\n );\n });\n\n return {\n modules: modulesData,\n tabs,\n };\n }\n\n /**\n * Get the module help template data.\n *\n * @param {Object} moduleData Data of the module to get help for.\n * @return {Promise} Promise resolved with the module help data.\n */\n async getModuleHelpTemplateData(moduleData) {\n const purposeKey = `mod_purpose_${moduleData.purpose}`;\n const allStrings = await loadNecessaryStrings();\n const data = {\n ...moduleData,\n };\n data.details = [\n {\n label: allStrings['supports'],\n value: allStrings[purposeKey] || '',\n },\n {\n label: allStrings['gradable'],\n value: moduleData.gradable ? allStrings['yes'] : allStrings['no'],\n },\n ];\n return data;\n }\n\n /**\n * Get the favourite tab data.\n *\n * @param {Array|Map} modulesData Modules data to be used in the chooser.\n * @return {Promise} Promise resolved with the template data.\n */\n async getFavouriteTabData(modulesData) {\n modulesData = this.normaliseModulesData(modulesData);\n const allStrings = await loadNecessaryStrings();\n\n // We need to deconstruct the modules data to ensure it is an array.\n const favouriteModules = modulesData.filter(\n mod => {\n return mod.favourite === true;\n }\n );\n\n return this.getTabData(\n 'favourites',\n favouriteModules,\n allStrings.favourites,\n null,\n favouriteModules.length > 0,\n );\n }\n\n /**\n * Get the search result template data.\n *\n * @param {Array|Map} resultsModulesData Modules data to be used in the chooser.\n * @return {Object} The template data.\n */\n getSearchResultData(resultsModulesData) {\n resultsModulesData = this.normaliseModulesData(resultsModulesData);\n return {\n 'searchresultsnumber': resultsModulesData.length,\n 'searchresults': resultsModulesData,\n };\n }\n\n /**\n * Get the number of items in a tab.\n *\n * @param {TabData} tabData The tab data.\n * @return {Number} The number of items in the tab.\n */\n countTabItems(tabData) {\n return tabData.items?.length ?? 0;\n }\n\n /**\n * Get the activity chooser footer template data.\n * @param {Object} footerData The active footer data object.\n * @return {Object} The template data.\n */\n getFooterData(footerData) {\n return {\n 'activeFooter': footerData.customfootertemplate,\n };\n }\n}\n\n/**\n * Load the necessary strings for the activity chooser.\n *\n * @return {Promise} Promise resolved with the loaded strings.\n */\nasync function loadNecessaryStrings() {\n if (allStrings !== null) {\n return allStrings;\n }\n allStrings = {};\n\n const stringToLoad = [\n {key: 'all', component: 'core'},\n {key: 'yes', component: 'core'},\n {key: 'no', component: 'core'},\n {key: 'favourites', component: 'core'},\n {key: 'recommended', component: 'core'},\n {key: 'gradable', component: 'core'},\n {key: 'recommended_help', component: 'core_course'},\n {key: 'supports', component: 'core_course'},\n ...activityCategories.map(\n (key) => ({\n key: 'mod_purpose_' + key,\n component: 'core_course',\n })\n ),\n ...activityCategories.map(\n (key) => ({\n key: 'mod_purpose_' + key + '_help',\n component: 'core_course',\n })\n ),\n ];\n\n const loadedStrings = await getStrings(stringToLoad);\n stringToLoad.forEach(({key}, index) => {\n allStrings[key] = loadedStrings[index];\n });\n return allStrings;\n}\n"],"names":["activityCategories","allStrings","loadNecessaryStrings","stringToLoad","key","component","map","loadedStrings","forEach","index","getTabData","tabId","filteredModules","tabLabel","tabHelp","result","active","items","displayed","length","normaliseModulesData","modulesData","Map","Array","from","values","isArray","Error","this","favouriteTab","getFavouriteTabData","tabs","all","hasSearchResults","filter","mod","recommended","recommended_help","separator","category","categoryModules","purpose","push","modules","moduleData","purposeKey","data","details","label","value","gradable","favouriteModules","favourite","favourites","getSearchResultData","resultsModulesData","countTabItems","tabData","_tabData$items","getFooterData","footerData","customfootertemplate"],"mappings":";;;;;;;;MAyBMA,mBAAqB,CACvB,iBACA,aACA,gBACA,gBACA,UACA,0BAGAC,WAAa,KAEjBC,sCA6MeA,0BACQ,OAAfD,kBACOA,WAEXA,WAAa,SAEPE,aAAe,CACjB,CAACC,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,KAAMC,UAAW,QACvB,CAACD,IAAK,aAAcC,UAAW,QAC/B,CAACD,IAAK,cAAeC,UAAW,QAChC,CAACD,IAAK,WAAYC,UAAW,QAC7B,CAACD,IAAK,mBAAoBC,UAAW,eACrC,CAACD,IAAK,WAAYC,UAAW,kBAC1BL,mBAAmBM,KACjBF,OACGA,IAAK,eAAiBA,IACtBC,UAAW,qBAGhBL,mBAAmBM,KACjBF,OACGA,IAAK,eAAiBA,IAAM,QAC5BC,UAAW,mBAKjBE,oBAAsB,mBAAWJ,qBACvCA,aAAaK,SAAQ,MAAQC,aAAPL,IAACA,UACnBH,WAAWG,KAAOG,cAAcE,UAE7BR,yCApNPS,WAAWC,MAAOC,gBAAiBC,cAAUC,+DAAU,WAC7CC,OAAS,CACXJ,MAAOA,MACPK,+DACAC,MAAOL,gBACPM,UAAWN,gBAAgBO,OAAS,EACpCN,SAAAA,iBAEAC,UACAC,OAAOD,QAAUA,SAEdC,OAYXK,qBAAqBC,gBACbA,uBAAuBC,IACvBD,YAAcE,MAAMC,KAAKH,YAAYI,eAClC,IAAKF,MAAMG,QAAQL,mBAChB,IAAIM,MAAM,mEAEbN,4CASqBA,aAC5BA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBACnB2B,mBAAqBD,KAAKE,oBAAoBT,aAE9CU,KAAO,CACT,IACOH,KAAKlB,WACJ,MACAW,YACApB,WAAW+B,IACX,MACCH,aAAaX,WAElBe,kBAAkB,GAEtBJ,aACA,IACOD,KAAKlB,WACJ,cACAW,YAAYa,QAAOC,MAA2B,IAApBA,IAAIC,cAC9BnC,WAAWmC,YACXnC,WAAWoC,kBAEfC,WAAW,WAInBtC,mBAAmBQ,SAAS+B,iBAClBC,gBAAkBnB,YAAYa,QAAOC,KAAOA,IAAIM,SAAWF,WAClC,IAA3BC,gBAAgBrB,QAGpBY,KAAKW,KACDd,KAAKlB,WACD6B,SACAC,gBACAvC,WAAW,eAAiBsC,UAC5BtC,WAAW,eAAiBsC,SAAW,cAK5C,CACHI,QAAStB,YACTU,KAAAA,sCAUwBa,kBACtBC,iCAA4BD,WAAWH,SACvCxC,iBAAmBC,uBACnB4C,KAAO,IACNF,mBAEPE,KAAKC,QAAU,CACX,CACIC,MAAO/C,WAAU,SACjBgD,MAAOhD,WAAW4C,aAAe,IAErC,CACIG,MAAO/C,WAAU,SACjBgD,MAAOL,WAAWM,SAAWjD,WAAU,IAAUA,WAAU,KAG5D6C,+BASezB,aACtBA,YAAcO,KAAKR,qBAAqBC,mBAClCpB,iBAAmBC,uBAGnBiD,iBAAmB9B,YAAYa,QACjCC,MAC6B,IAAlBA,IAAIiB,mBAIZxB,KAAKlB,WACR,aACAyC,iBACAlD,WAAWoD,WACX,KACAF,iBAAiBhC,OAAS,GAUlCmC,oBAAoBC,0BAET,sBADPA,mBAAqB3B,KAAKR,qBAAqBmC,qBAEDpC,qBACzBoC,oBAUzBC,cAAcC,8GACHA,QAAQxC,uCAARyC,eAAevC,8DAAU,EAQpCwC,cAAcC,kBACH,cACaA,WAAWC"} \ No newline at end of file diff --git a/public/course/amd/src/local/activitychooser/exporter.js b/public/course/amd/src/local/activitychooser/exporter.js index 60b3ce65112..235429c0d1a 100644 --- a/public/course/amd/src/local/activitychooser/exporter.js +++ b/public/course/amd/src/local/activitychooser/exporter.js @@ -164,6 +164,10 @@ export default class { label: allStrings['supports'], value: allStrings[purposeKey] || '', }, + { + label: allStrings['gradable'], + value: moduleData.gradable ? allStrings['yes'] : allStrings['no'], + }, ]; return data; } @@ -243,8 +247,11 @@ async function loadNecessaryStrings() { const stringToLoad = [ {key: 'all', component: 'core'}, + {key: 'yes', component: 'core'}, + {key: 'no', component: 'core'}, {key: 'favourites', component: 'core'}, {key: 'recommended', component: 'core'}, + {key: 'gradable', component: 'core'}, {key: 'recommended_help', component: 'core_course'}, {key: 'supports', component: 'core_course'}, ...activityCategories.map( diff --git a/public/course/classes/local/entity/content_item.php b/public/course/classes/local/entity/content_item.php index 742b104bb93..229eb46c4ea 100644 --- a/public/course/classes/local/entity/content_item.php +++ b/public/course/classes/local/entity/content_item.php @@ -33,36 +33,6 @@ defined('MOODLE_INTERNAL') || die(); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content_item { - /** @var int $id the id. */ - private $id; - - /** @var string $name the name. */ - private $name; - - /** @var title $title the title. */ - private $title; - - /** @var \moodle_url $link the url for the content item's setup page (usually mod/edit.php). */ - private $link; - - /** @var string $icon an html string containing the icon for this item. */ - private $icon; - - /** @var string $help the description/help text for this content item. */ - private $help; - - /** @var int $achetype a module archetype, e.g. MOD_ARCHETYPE_RESOURCE, MOD_ARCHETYPE_OTHER. */ - private $archetype; - - /** @var string $componentname the name of the component from which this content item originates. */ - private $componentname; - - /** @var string $purpose the purpose type of this component. */ - private $purpose; - - /** @var bool $branded whether or not this component is branded. */ - private $branded; - /** * The content_item constructor. * @@ -76,19 +46,32 @@ class content_item { * @param string $componentname the name of the component/plugin with which this content item is associated. * @param string $purpose the purpose type of this component. * @param bool $branded whether or not this item is branded. + * @param bool $gradable whether or not this item is gradable. */ - public function __construct(int $id, string $name, title $title, \moodle_url $link, string $icon, string $help, - int $archetype, string $componentname, string $purpose, bool $branded = false) { - $this->id = $id; - $this->name = $name; - $this->title = $title; - $this->link = $link; - $this->icon = $icon; - $this->help = $help; - $this->archetype = $archetype; - $this->componentname = $componentname; - $this->purpose = $purpose; - $this->branded = $branded; + public function __construct( + /** @var int $id the id. */ + private int $id, + /** @var string $name the name. */ + private string $name, + /** @var title $title the title. */ + private title $title, + /** @var \moodle_url $link the url for the content item's setup page (usually mod/edit.php). */ + private \moodle_url $link, + /** @var string $icon an html string containing the icon for this item. */ + private string $icon, + /** @var string $help the description/help text for this content item. */ + private string $help, + /** @var int $archetype a module archetype, e.g. MOD_ARCHETYPE_RESOURCE, MOD_ARCHETYPE_OTHER. */ + private int $archetype, + /** @var string $componentname the name of the component from which this content item originates. */ + private string $componentname, + /** @var string $purpose the purpose type of this component. */ + private string $purpose, + /** @var bool $branded whether or not this component is branded. */ + private bool $branded = false, + /** @var bool $gradable whether or not this component is gradable. */ + private bool $gradable = false, + ) { } /** @@ -179,4 +162,13 @@ class content_item { public function is_branded(): bool { return $this->branded; } + + /** + * Whether this item is gradable. + * + * @return bool true if this item is gradable, false otherwise. + */ + public function is_gradable(): bool { + return $this->gradable; + } } diff --git a/public/course/classes/local/exporters/course_content_item_exporter.php b/public/course/classes/local/exporters/course_content_item_exporter.php index 4106b418147..0c59955a059 100644 --- a/public/course/classes/local/exporters/course_content_item_exporter.php +++ b/public/course/classes/local/exporters/course_content_item_exporter.php @@ -70,6 +70,7 @@ class course_content_item_exporter extends exporter { 'componentname' => ['type' => PARAM_TEXT, 'description' => 'The name of the component exposing the content item'], 'purpose' => ['type' => PARAM_TEXT, 'description' => 'The purpose of the component exposing the content item'], 'branded' => ['type' => PARAM_BOOL, 'description' => ' Whether this content item is branded or not'], + 'gradable' => ['type' => PARAM_BOOL, 'description' => 'Whether this content item is gradable or not'], ]; } @@ -141,6 +142,7 @@ class course_content_item_exporter extends exporter { 'recommended' => $recommended, 'purpose' => $this->contentitem->get_purpose(), 'branded' => $this->contentitem->is_branded(), + 'gradable' => $this->contentitem->is_gradable(), ]; return $properties; diff --git a/public/course/classes/local/repository/content_item_readonly_repository.php b/public/course/classes/local/repository/content_item_readonly_repository.php index 0cdb7485124..7f99b35a2f8 100644 --- a/public/course/classes/local/repository/content_item_readonly_repository.php +++ b/public/course/classes/local/repository/content_item_readonly_repository.php @@ -146,18 +146,20 @@ class content_item_readonly_repository implements content_item_readonly_reposito $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); $purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER); $isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false); + $gradable = plugin_supports('mod', $mod->name, FEATURE_GRADE_HAS_GRADE, false); $contentitem = new content_item( - $mod->id, - $mod->name, - new lang_string_title("modulename", $mod->name), - new \moodle_url(''), // No course scope, so just an empty link. - $OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']), - $help, - $archetype, - 'mod_' . $mod->name, - $purpose, - $isbranded, + id: $mod->id, + name: $mod->name, + title: new lang_string_title("modulename", $mod->name), + link: new \moodle_url(''), // No course scope, so just an empty link. + icon: $OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']), + help: $help, + archetype: $archetype, + componentname: 'mod_' . $mod->name, + purpose: $purpose, + branded: $isbranded, + gradable: $gradable, ); $modcontentitemreference = clone($contentitem); @@ -212,6 +214,7 @@ class content_item_readonly_repository implements content_item_readonly_reposito $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); $purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER); $isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false); + $gradable = plugin_supports('mod', $mod->name, FEATURE_GRADE_HAS_GRADE, false); $icon = 'monologo'; // Quick check for monologo icons. @@ -222,16 +225,17 @@ class content_item_readonly_repository implements content_item_readonly_reposito $iconclass = 'nofilter'; } $contentitem = new content_item( - $mod->id, - $mod->name, - new lang_string_title("modulename", $mod->name), - new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]), - $OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]), - $help, - $archetype, - 'mod_' . $mod->name, - $purpose, - $isbranded, + id: $mod->id, + name: $mod->name, + title: new lang_string_title("modulename", $mod->name), + link: new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]), + icon: $OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]), + help: $help, + archetype: $archetype, + componentname: 'mod_' . $mod->name, + purpose: $purpose, + branded: $isbranded, + gradable: $gradable, ); $modcontentitemreference = clone($contentitem); diff --git a/public/course/tests/behat/activity_chooser.feature b/public/course/tests/behat/activity_chooser.feature index eeee315ab7a..0a65018b8c5 100644 --- a/public/course/tests/behat/activity_chooser.feature +++ b/public/course/tests/behat/activity_chooser.feature @@ -47,7 +47,18 @@ Feature: Display and choose from the available activities in course When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue" Then I should see "Assignment" in the "help" "core_course > Activity chooser screen" And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." + And I should see "Supports" in the "help" "core_course > Activity chooser screen" And I should see "Assessment" in the "help" "core_course > Activity chooser screen" + And I should see "Gradable" in the "help" "core_course > Activity chooser screen" + And I should see "Yes" in the "help" "core_course > Activity chooser screen" + And I click on "Back" "button" in the "Add an activity or resource" "dialogue" + And I click on "Information about the Book activity" "button" in the "Add an activity or resource" "dialogue" + Then I should see "Book" in the "help" "core_course > Activity chooser screen" + And I should see "The book module enables a teacher to create a multi-page resource in a book-like format" + And I should see "Supports" in the "help" "core_course > Activity chooser screen" + And I should see "Resources" in the "help" "core_course > Activity chooser screen" + And I should see "Gradable" in the "help" "core_course > Activity chooser screen" + And I should see "No" in the "help" "core_course > Activity chooser screen" # Confirm show summary also works for weekly format course And I am on "C2" course homepage with editing mode on And I click on "Add content" "button" in the "13 January - 19 January" "section" diff --git a/public/course/tests/content_item_test.php b/public/course/tests/content_item_test.php index b134a06dc5f..f1ddaa3c60c 100644 --- a/public/course/tests/content_item_test.php +++ b/public/course/tests/content_item_test.php @@ -39,9 +39,19 @@ final class content_item_test extends \advanced_testcase { public function test_content_item(): void { $this->resetAfterTest(); - $contentitem = new content_item(22, 'Item name', new lang_string_title('modulename', 'mod_assign'), - new \moodle_url('mod_edit.php'), '', 'Description of the module', MOD_ARCHETYPE_RESOURCE, 'mod_page', - MOD_PURPOSE_CONTENT, true); + $contentitem = new content_item( + id: 22, + name: 'Item name', + title: new lang_string_title('modulename', 'mod_assign'), + link: new \moodle_url('mod_edit.php'), + icon: '', + help: 'Description of the module', + archetype: MOD_ARCHETYPE_RESOURCE, + componentname: 'mod_page', + purpose: MOD_PURPOSE_CONTENT, + branded: true, + gradable: true, + ); $this->assertEquals(22, $contentitem->get_id()); $this->assertEquals('Item name', $contentitem->get_name()); @@ -53,6 +63,7 @@ final class content_item_test extends \advanced_testcase { $this->assertEquals('mod_page', $contentitem->get_component_name()); $this->assertEquals('content', $contentitem->get_purpose()); $this->assertTrue($contentitem->is_branded()); + $this->assertTrue($contentitem->is_gradable()); } /** diff --git a/public/lang/en/moodle.php b/public/lang/en/moodle.php index e013763d773..5eb4402730f 100644 --- a/public/lang/en/moodle.php +++ b/public/lang/en/moodle.php @@ -991,6 +991,7 @@ This program is free software: you can redistribute it and/or modify it under th This program 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 Moodle License information page for full details: https://moodledev.io/general/license'; +$string['gradable'] = 'Gradable'; $string['grades'] = 'Grades'; $string['gradenoun'] = 'Grade'; $string['gradeverb'] = 'Grade';