From 7b8a5bfd50b2320eeb6cc9896b7d5c9c3d7131ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Monlla=C3=B3?= Date: Thu, 16 May 2019 15:54:41 +0200 Subject: [PATCH 1/3] MDL-65588 tool_analytics: Alternative method to pass data to JS The more time-splitting methods we have in core the more data we pass to the amd module as an argument. There is a limit of 1024 characters and a debugging message is triggered by js_call_amd if we surpass it. --- admin/tool/analytics/amd/build/model.min.js | 2 +- admin/tool/analytics/amd/build/model.min.js.map | 2 +- admin/tool/analytics/amd/src/model.js | 6 ++++-- admin/tool/analytics/classes/output/models_list.php | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/admin/tool/analytics/amd/build/model.min.js b/admin/tool/analytics/amd/build/model.min.js index 9cdfe5e6d644d..05c30735ae06d 100644 --- a/admin/tool/analytics/amd/build/model.min.js +++ b/admin/tool/analytics/amd/build/model.min.js @@ -1,2 +1,2 @@ -define ("tool_analytics/model",["jquery","core/str","core/log","core/notification","core/modal_factory","core/modal_events","core/templates"],function(b,c,d,e,f,g,h){var i={clear:{title:{key:"clearpredictions",component:"tool_analytics"},body:{key:"clearmodelpredictions",component:"tool_analytics"}},delete:{title:{key:"delete",component:"tool_analytics"},body:{key:"deletemodelconfirmation",component:"tool_analytics"}}},j=function(a){var c=b(a).closest("[data-model-name]");if(c.length){return c.attr("data-model-name")}else{d.error("Unexpected DOM error - unable to obtain the model name");return""}};return{confirmAction:function confirmAction(a,h){b("[data-action-id=\""+a+"\"]").on("click",function(k){k.preventDefault();var l=b(k.currentTarget);if("undefined"==typeof i[h]){d.error("Action \""+h+"\" is not allowed.");return}var a=[i[h].title,i[h].body];a[1].param=j(l);var m=c.get_strings(a),n=f.create({type:f.types.SAVE_CANCEL});b.when(m,n).then(function(a,b){b.setTitle(a[0]);b.setBody(a[1]);b.setSaveButtonText(a[0]);b.getRoot().on(g.save,function(){window.location.href=l.attr("href")});b.show();return b}).fail(e.exception)})},selectEvaluationOptions:function selectEvaluationOptions(a,d,i){b("[data-action-id=\""+a+"\"]").on("click",function(j){j.preventDefault();var k=b(j.currentTarget),a=c.get_strings([{key:"evaluatemodel",component:"tool_analytics"},{key:"evaluate",component:"tool_analytics"}]),l=f.create({type:f.types.SAVE_CANCEL}),m=h.render("tool_analytics/evaluation_options",{trainedexternally:d,timesplittingmethods:i});b.when(a,l).then(function(a,c){c.getRoot().on(g.hidden,c.destroy.bind(c));c.setTitle(a[0]);c.setSaveButtonText(a[1]);c.setBody(m);c.getRoot().on(g.save,function(){var a=b("input[name='evaluationmode']:checked").val();if("trainedmodel"==a){k.attr("href",k.attr("href")+"&mode=trainedmodel")}var c=b("#id-evaluation-timesplitting").val();k.attr("href",k.attr("href")+"×plitting="+c);window.location.href=k.attr("href")});c.show();return c}).fail(e.exception)})},selectExportOptions:function selectExportOptions(a,d){b("[data-action-id=\""+a+"\"]").on("click",function(i){i.preventDefault();var j=b(i.currentTarget);if(!d){j.attr("href",j.attr("href")+"&action=exportmodel&includeweights=0");window.location.href=j.attr("href");return}var a=c.get_strings([{key:"export",component:"tool_analytics"}]),k=f.create({type:f.types.SAVE_CANCEL}),l=h.render("tool_analytics/export_options",{});b.when(a,k).then(function(a,c){c.getRoot().on(g.hidden,c.destroy.bind(c));c.setTitle(a[0]);c.setSaveButtonText(a[0]);c.setBody(l);c.getRoot().on(g.save,function(){var a=b("input[name='exportoption']:checked").val();if("exportdata"==a){j.attr("href",j.attr("href")+"&action=exportdata")}else{j.attr("href",j.attr("href")+"&action=exportmodel");if(b("#id-includeweights").is(":checked")){j.attr("href",j.attr("href")+"&includeweights=1")}else{j.attr("href",j.attr("href")+"&includeweights=0")}}window.location.href=j.attr("href")});c.show();return c}).fail(e.exception)})}}}); +define ("tool_analytics/model",["jquery","core/str","core/log","core/notification","core/modal_factory","core/modal_events","core/templates"],function(b,c,d,e,f,g,h){var i={clear:{title:{key:"clearpredictions",component:"tool_analytics"},body:{key:"clearmodelpredictions",component:"tool_analytics"}},delete:{title:{key:"delete",component:"tool_analytics"},body:{key:"deletemodelconfirmation",component:"tool_analytics"}}},j=function(a){var c=b(a).closest("[data-model-name]");if(c.length){return c.attr("data-model-name")}else{d.error("Unexpected DOM error - unable to obtain the model name");return""}};return{confirmAction:function confirmAction(a,h){b("[data-action-id=\""+a+"\"]").on("click",function(k){k.preventDefault();var l=b(k.currentTarget);if("undefined"==typeof i[h]){d.error("Action \""+h+"\" is not allowed.");return}var a=[i[h].title,i[h].body];a[1].param=j(l);var m=c.get_strings(a),n=f.create({type:f.types.SAVE_CANCEL});b.when(m,n).then(function(a,b){b.setTitle(a[0]);b.setBody(a[1]);b.setSaveButtonText(a[0]);b.getRoot().on(g.save,function(){window.location.href=l.attr("href")});b.show();return b}).fail(e.exception)})},selectEvaluationOptions:function selectEvaluationOptions(a,d){b("[data-action-id=\""+a+"\"]").on("click",function(i){i.preventDefault();var j=b(i.currentTarget),a=b(this).attr("data-timesplitting-methods"),k=c.get_strings([{key:"evaluatemodel",component:"tool_analytics"},{key:"evaluate",component:"tool_analytics"}]),l=f.create({type:f.types.SAVE_CANCEL}),m=h.render("tool_analytics/evaluation_options",{trainedexternally:d,timesplittingmethods:JSON.parse(a)});b.when(k,l).then(function(a,c){c.getRoot().on(g.hidden,c.destroy.bind(c));c.setTitle(a[0]);c.setSaveButtonText(a[1]);c.setBody(m);c.getRoot().on(g.save,function(){var a=b("input[name='evaluationmode']:checked").val();if("trainedmodel"==a){j.attr("href",j.attr("href")+"&mode=trainedmodel")}var c=b("#id-evaluation-timesplitting").val();j.attr("href",j.attr("href")+"×plitting="+c);window.location.href=j.attr("href")});c.show();return c}).fail(e.exception)})},selectExportOptions:function selectExportOptions(a,d){b("[data-action-id=\""+a+"\"]").on("click",function(i){i.preventDefault();var j=b(i.currentTarget);if(!d){j.attr("href",j.attr("href")+"&action=exportmodel&includeweights=0");window.location.href=j.attr("href");return}var a=c.get_strings([{key:"export",component:"tool_analytics"}]),k=f.create({type:f.types.SAVE_CANCEL}),l=h.render("tool_analytics/export_options",{});b.when(a,k).then(function(a,c){c.getRoot().on(g.hidden,c.destroy.bind(c));c.setTitle(a[0]);c.setSaveButtonText(a[0]);c.setBody(l);c.getRoot().on(g.save,function(){var a=b("input[name='exportoption']:checked").val();if("exportdata"==a){j.attr("href",j.attr("href")+"&action=exportdata")}else{j.attr("href",j.attr("href")+"&action=exportmodel");if(b("#id-includeweights").is(":checked")){j.attr("href",j.attr("href")+"&includeweights=1")}else{j.attr("href",j.attr("href")+"&includeweights=0")}}window.location.href=j.attr("href")});c.show();return c}).fail(e.exception)})}}}); //# sourceMappingURL=model.min.js.map diff --git a/admin/tool/analytics/amd/build/model.min.js.map b/admin/tool/analytics/amd/build/model.min.js.map index fab51a0a77dfa..1ea039bdf3ba1 100644 --- a/admin/tool/analytics/amd/build/model.min.js.map +++ b/admin/tool/analytics/amd/build/model.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/model.js"],"names":["define","$","Str","log","Notification","ModalFactory","ModalEvents","Templates","actionsList","clear","title","key","component","body","getModelName","actionItem","wrap","closest","length","attr","error","confirmAction","actionId","actionType","on","ev","preventDefault","a","currentTarget","reqStrings","param","stringsPromise","get_strings","modalPromise","create","type","types","SAVE_CANCEL","when","then","strings","modal","setTitle","setBody","setSaveButtonText","getRoot","save","window","location","href","show","fail","exception","selectEvaluationOptions","trainedOnlyExternally","timeSplittingMethods","bodyPromise","render","trainedexternally","timesplittingmethods","hidden","destroy","bind","evaluationMode","val","timeSplittingMethod","selectExportOptions","isTrained","exportOption","is"],"mappings":"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","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 * AMD module for model actions confirmation.\n *\n * @module tool_analytics/model\n * @copyright 2017 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/templates'],\n function($, Str, log, Notification, ModalFactory, ModalEvents, Templates) {\n\n /**\n * List of actions that require confirmation and confirmation message.\n */\n var actionsList = {\n clear: {\n title: {\n key: 'clearpredictions',\n component: 'tool_analytics'\n }, body: {\n key: 'clearmodelpredictions',\n component: 'tool_analytics'\n }\n\n },\n 'delete': {\n title: {\n key: 'delete',\n component: 'tool_analytics'\n }, body: {\n key: 'deletemodelconfirmation',\n component: 'tool_analytics'\n }\n }\n };\n\n /**\n * Returns the model name.\n *\n * @param {Object} actionItem The action item DOM node.\n * @return {String}\n */\n var getModelName = function(actionItem) {\n var wrap = $(actionItem).closest('[data-model-name]');\n\n if (wrap.length) {\n return wrap.attr('data-model-name');\n\n } else {\n log.error('Unexpected DOM error - unable to obtain the model name');\n return '';\n }\n };\n\n /** @alias module:tool_analytics/model */\n return {\n\n /**\n * Displays a confirm modal window before executing the action.\n *\n * @param {String} actionId\n * @param {String} actionType\n */\n confirmAction: function(actionId, actionType) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n if (typeof actionsList[actionType] === \"undefined\") {\n log.error('Action \"' + actionType + '\" is not allowed.');\n return;\n }\n\n var reqStrings = [\n actionsList[actionType].title,\n actionsList[actionType].body\n ];\n reqStrings[1].param = getModelName(a);\n\n var stringsPromise = Str.get_strings(reqStrings);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n modal.setTitle(strings[0]);\n modal.setBody(strings[1]);\n modal.setSaveButtonText(strings[0]);\n modal.getRoot().on(ModalEvents.save, function() {\n window.location.href = a.attr('href');\n });\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n },\n\n /**\n * Displays evaluation mode and time-splitting method choices.\n *\n * @param {String} actionId\n * @param {Boolean} trainedOnlyExternally\n */\n selectEvaluationOptions: function(actionId, trainedOnlyExternally, timeSplittingMethods) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n var stringsPromise = Str.get_strings([\n {\n key: 'evaluatemodel',\n component: 'tool_analytics'\n }, {\n key: 'evaluate',\n component: 'tool_analytics'\n }\n ]);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n var bodyPromise = Templates.render('tool_analytics/evaluation_options', {\n trainedexternally: trainedOnlyExternally,\n timesplittingmethods: timeSplittingMethods\n });\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n\n\n modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));\n\n modal.setTitle(strings[0]);\n modal.setSaveButtonText(strings[1]);\n modal.setBody(bodyPromise);\n\n modal.getRoot().on(ModalEvents.save, function() {\n\n // Evaluation mode.\n var evaluationMode = $(\"input[name='evaluationmode']:checked\").val();\n if (evaluationMode == 'trainedmodel') {\n a.attr('href', a.attr('href') + '&mode=trainedmodel');\n }\n\n // Selected time-splitting id.\n var timeSplittingMethod = $(\"#id-evaluation-timesplitting\").val();\n a.attr('href', a.attr('href') + '×plitting=' + timeSplittingMethod);\n\n window.location.href = a.attr('href');\n return;\n });\n\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n },\n\n /**\n * Displays export options.\n *\n * We have two main options: export training data and export configuration.\n * The 2nd option has an extra option: include the trained algorithm weights.\n *\n * @param {String} actionId\n * @param {Boolean} isTrained\n */\n selectExportOptions: function(actionId, isTrained) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n if (!isTrained) {\n // Export the model configuration if the model is not trained. We can't export anything else.\n a.attr('href', a.attr('href') + '&action=exportmodel&includeweights=0');\n window.location.href = a.attr('href');\n return;\n }\n\n var stringsPromise = Str.get_strings([\n {\n key: 'export',\n component: 'tool_analytics'\n }\n ]);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n var bodyPromise = Templates.render('tool_analytics/export_options', {});\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n\n modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));\n\n modal.setTitle(strings[0]);\n modal.setSaveButtonText(strings[0]);\n modal.setBody(bodyPromise);\n\n modal.getRoot().on(ModalEvents.save, function() {\n\n var exportOption = $(\"input[name='exportoption']:checked\").val();\n\n if (exportOption == 'exportdata') {\n a.attr('href', a.attr('href') + '&action=exportdata');\n\n } else {\n a.attr('href', a.attr('href') + '&action=exportmodel');\n if ($(\"#id-includeweights\").is(':checked')) {\n a.attr('href', a.attr('href') + '&includeweights=1');\n } else {\n a.attr('href', a.attr('href') + '&includeweights=0');\n }\n }\n\n window.location.href = a.attr('href');\n return;\n });\n\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n }\n };\n});\n"],"file":"model.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/model.js"],"names":["define","$","Str","log","Notification","ModalFactory","ModalEvents","Templates","actionsList","clear","title","key","component","body","getModelName","actionItem","wrap","closest","length","attr","error","confirmAction","actionId","actionType","on","ev","preventDefault","a","currentTarget","reqStrings","param","stringsPromise","get_strings","modalPromise","create","type","types","SAVE_CANCEL","when","then","strings","modal","setTitle","setBody","setSaveButtonText","getRoot","save","window","location","href","show","fail","exception","selectEvaluationOptions","trainedOnlyExternally","timeSplittingMethods","bodyPromise","render","trainedexternally","timesplittingmethods","JSON","parse","hidden","destroy","bind","evaluationMode","val","timeSplittingMethod","selectExportOptions","isTrained","exportOption","is"],"mappings":"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","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 * AMD module for model actions confirmation.\n *\n * @module tool_analytics/model\n * @copyright 2017 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/templates'],\n function($, Str, log, Notification, ModalFactory, ModalEvents, Templates) {\n\n /**\n * List of actions that require confirmation and confirmation message.\n */\n var actionsList = {\n clear: {\n title: {\n key: 'clearpredictions',\n component: 'tool_analytics'\n }, body: {\n key: 'clearmodelpredictions',\n component: 'tool_analytics'\n }\n\n },\n 'delete': {\n title: {\n key: 'delete',\n component: 'tool_analytics'\n }, body: {\n key: 'deletemodelconfirmation',\n component: 'tool_analytics'\n }\n }\n };\n\n /**\n * Returns the model name.\n *\n * @param {Object} actionItem The action item DOM node.\n * @return {String}\n */\n var getModelName = function(actionItem) {\n var wrap = $(actionItem).closest('[data-model-name]');\n\n if (wrap.length) {\n return wrap.attr('data-model-name');\n\n } else {\n log.error('Unexpected DOM error - unable to obtain the model name');\n return '';\n }\n };\n\n /** @alias module:tool_analytics/model */\n return {\n\n /**\n * Displays a confirm modal window before executing the action.\n *\n * @param {String} actionId\n * @param {String} actionType\n */\n confirmAction: function(actionId, actionType) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n if (typeof actionsList[actionType] === \"undefined\") {\n log.error('Action \"' + actionType + '\" is not allowed.');\n return;\n }\n\n var reqStrings = [\n actionsList[actionType].title,\n actionsList[actionType].body\n ];\n reqStrings[1].param = getModelName(a);\n\n var stringsPromise = Str.get_strings(reqStrings);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n modal.setTitle(strings[0]);\n modal.setBody(strings[1]);\n modal.setSaveButtonText(strings[0]);\n modal.getRoot().on(ModalEvents.save, function() {\n window.location.href = a.attr('href');\n });\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n },\n\n /**\n * Displays evaluation mode and time-splitting method choices.\n *\n * @param {String} actionId\n * @param {Boolean} trainedOnlyExternally\n */\n selectEvaluationOptions: function(actionId, trainedOnlyExternally) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n var timeSplittingMethods = $(this).attr('data-timesplitting-methods');\n\n var stringsPromise = Str.get_strings([\n {\n key: 'evaluatemodel',\n component: 'tool_analytics'\n }, {\n key: 'evaluate',\n component: 'tool_analytics'\n }\n ]);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n var bodyPromise = Templates.render('tool_analytics/evaluation_options', {\n trainedexternally: trainedOnlyExternally,\n timesplittingmethods: JSON.parse(timeSplittingMethods)\n });\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n\n\n modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));\n\n modal.setTitle(strings[0]);\n modal.setSaveButtonText(strings[1]);\n modal.setBody(bodyPromise);\n\n modal.getRoot().on(ModalEvents.save, function() {\n\n // Evaluation mode.\n var evaluationMode = $(\"input[name='evaluationmode']:checked\").val();\n if (evaluationMode == 'trainedmodel') {\n a.attr('href', a.attr('href') + '&mode=trainedmodel');\n }\n\n // Selected time-splitting id.\n var timeSplittingMethod = $(\"#id-evaluation-timesplitting\").val();\n a.attr('href', a.attr('href') + '×plitting=' + timeSplittingMethod);\n\n window.location.href = a.attr('href');\n return;\n });\n\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n },\n\n /**\n * Displays export options.\n *\n * We have two main options: export training data and export configuration.\n * The 2nd option has an extra option: include the trained algorithm weights.\n *\n * @param {String} actionId\n * @param {Boolean} isTrained\n */\n selectExportOptions: function(actionId, isTrained) {\n $('[data-action-id=\"' + actionId + '\"]').on('click', function(ev) {\n ev.preventDefault();\n\n var a = $(ev.currentTarget);\n\n if (!isTrained) {\n // Export the model configuration if the model is not trained. We can't export anything else.\n a.attr('href', a.attr('href') + '&action=exportmodel&includeweights=0');\n window.location.href = a.attr('href');\n return;\n }\n\n var stringsPromise = Str.get_strings([\n {\n key: 'export',\n component: 'tool_analytics'\n }\n ]);\n var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});\n var bodyPromise = Templates.render('tool_analytics/export_options', {});\n\n $.when(stringsPromise, modalPromise).then(function(strings, modal) {\n\n modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));\n\n modal.setTitle(strings[0]);\n modal.setSaveButtonText(strings[0]);\n modal.setBody(bodyPromise);\n\n modal.getRoot().on(ModalEvents.save, function() {\n\n var exportOption = $(\"input[name='exportoption']:checked\").val();\n\n if (exportOption == 'exportdata') {\n a.attr('href', a.attr('href') + '&action=exportdata');\n\n } else {\n a.attr('href', a.attr('href') + '&action=exportmodel');\n if ($(\"#id-includeweights\").is(':checked')) {\n a.attr('href', a.attr('href') + '&includeweights=1');\n } else {\n a.attr('href', a.attr('href') + '&includeweights=0');\n }\n }\n\n window.location.href = a.attr('href');\n return;\n });\n\n modal.show();\n return modal;\n }).fail(Notification.exception);\n });\n }\n };\n});\n"],"file":"model.min.js"} \ No newline at end of file diff --git a/admin/tool/analytics/amd/src/model.js b/admin/tool/analytics/amd/src/model.js index 919993c1f942f..966c6cc416ef2 100644 --- a/admin/tool/analytics/amd/src/model.js +++ b/admin/tool/analytics/amd/src/model.js @@ -114,12 +114,14 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto * @param {String} actionId * @param {Boolean} trainedOnlyExternally */ - selectEvaluationOptions: function(actionId, trainedOnlyExternally, timeSplittingMethods) { + selectEvaluationOptions: function(actionId, trainedOnlyExternally) { $('[data-action-id="' + actionId + '"]').on('click', function(ev) { ev.preventDefault(); var a = $(ev.currentTarget); + var timeSplittingMethods = $(this).attr('data-timesplitting-methods'); + var stringsPromise = Str.get_strings([ { key: 'evaluatemodel', @@ -132,7 +134,7 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL}); var bodyPromise = Templates.render('tool_analytics/evaluation_options', { trainedexternally: trainedOnlyExternally, - timesplittingmethods: timeSplittingMethods + timesplittingmethods: JSON.parse(timeSplittingMethods) }); $.when(stringsPromise, modalPromise).then(function(strings, modal) { diff --git a/admin/tool/analytics/classes/output/models_list.php b/admin/tool/analytics/classes/output/models_list.php index 47cd54acd1aee..be1c7bae38683 100644 --- a/admin/tool/analytics/classes/output/models_list.php +++ b/admin/tool/analytics/classes/output/models_list.php @@ -222,12 +222,13 @@ public function export_for_template(\renderer_base $output) { array_unshift($modeltimesplittingmethods, $currenttimesplitting); } - $evaluateparams = [$actionid, $trainedonlyexternally, $modeltimesplittingmethods]; + $evaluateparams = [$actionid, $trainedonlyexternally]; $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationOptions', $evaluateparams); $urlparams['action'] = 'evaluate'; $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams); $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')), - get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid]); + get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid, + 'data-timesplitting-methods' => json_encode($modeltimesplittingmethods)]); $actionsmenu->add($icon); } From 7d8ed90757cc0500d2567b9f269764698d216b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Monlla=C3=B3?= Date: Thu, 25 Jul 2019 16:21:30 +0200 Subject: [PATCH 2/3] MDL-65588 analytics: Unit tests for any_course_access --- lib/tests/indicators_test.php | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lib/tests/indicators_test.php b/lib/tests/indicators_test.php index 4511766535832..e804dff48ee6d 100644 --- a/lib/tests/indicators_test.php +++ b/lib/tests/indicators_test.php @@ -48,6 +48,7 @@ class core_analytics_indicators_testcase extends advanced_testcase { * @return void */ public function test_core_indicators() { + global $DB; $this->preventResetByRollback(); $this->resetAfterTest(true); @@ -122,6 +123,65 @@ public function test_core_indicators() { $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]); + // Test any course access. + $course = $this->getDataGenerator()->create_course($params); + $coursecontext = \context_course::instance($course->id); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + + $indicator = new \core\analytics\indicator\any_course_access(); + + $sampleids = array($user1->id => $user1->id); + $data = array($user1->id => array( + 'course' => $course, + 'user' => $user1 + )); + $indicator->add_sample_data($data); + $analysable = new \core_analytics\course($course); + + // Min value if no user_lastaccess records. + $indicator->fill_per_analysable_caches($analysable); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere'); + $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', time() - 10, time() + 10); + $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]); + + // Any access is enough if no time restrictions. + $DB->insert_record('user_lastaccess', array('userid' => $user1->id, + 'courseid' => $course->id, 'timeaccess' => time() - 1)); + $indicator->fill_per_analysable_caches($analysable); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere'); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + + // Min value if the existing records are old. + $indicator->fill_per_analysable_caches($analysable); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', time(), time() + 10); + $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', time()); + $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]); + + // Max value if the existing records are prior to end. + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', time() - 10, time()); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', false, time()); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', false, time()); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + + // Max value if no end time and existing user_lastaccess record. + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', time() - 10); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + + // Rely on logs if the last time access is after the end time. + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', false, time() - 10); + // Min value if no logs are found. + $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]); + + \logstore_standard\event\unittest_executed::create( + array('context' => \context_course::instance($course->id), 'userid' => $user1->id))->trigger(); + // Max value if logs are found before the end time. + list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere', false, time() + 10); + $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]); + // Test any write action. $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); From 2d9280e0df03064f7e15187c2f7bd86555a35e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Monlla=C3=B3?= Date: Fri, 17 May 2019 12:24:13 +0200 Subject: [PATCH 3/3] MDL-65588 analytics: New models for student accesses --- .../local/time_splitting/after_start.php | 111 ++++++++++++++ .../local/time_splitting/past_periodic.php | 97 +++++++++++++ .../classes/local/time_splitting/periodic.php | 88 +++++++----- .../time_splitting/upcoming_periodic.php | 17 ++- .../fixtures/test_timesplitting_seconds.php | 2 +- analytics/tests/manager_test.php | 12 +- analytics/tests/stats_test.php | 6 +- analytics/upgrade.txt | 3 + .../target/no_access_since_course_start.php | 80 +++++++++++ .../analytics/target/no_recent_accesses.php | 135 ++++++++++++++++++ lang/en/course.php | 10 ++ lang/en/moodle.php | 14 ++ .../analytics/indicator/any_course_access.php | 135 ++++++++++++++++++ .../time_splitting/one_month_after_start.php | 56 ++++++++ .../time_splitting/one_week_after_start.php | 56 ++++++++ .../analytics/time_splitting/past_3_days.php | 55 +++++++ .../analytics/time_splitting/past_month.php | 55 +++++++ .../analytics/time_splitting/past_week.php | 7 +- .../ten_percent_after_start.php | 81 +++++++++++ lib/db/analytics.php | 16 +++ lib/tests/time_splittings_test.php | 132 ++++++++++++----- 21 files changed, 1079 insertions(+), 89 deletions(-) create mode 100644 analytics/classes/local/time_splitting/after_start.php create mode 100644 analytics/classes/local/time_splitting/past_periodic.php create mode 100644 course/classes/analytics/target/no_access_since_course_start.php create mode 100644 course/classes/analytics/target/no_recent_accesses.php create mode 100644 lib/classes/analytics/indicator/any_course_access.php create mode 100644 lib/classes/analytics/time_splitting/one_month_after_start.php create mode 100644 lib/classes/analytics/time_splitting/one_week_after_start.php create mode 100644 lib/classes/analytics/time_splitting/past_3_days.php create mode 100644 lib/classes/analytics/time_splitting/past_month.php rename analytics/tests/fixtures/test_timesplitting_weekly.php => lib/classes/analytics/time_splitting/past_week.php (89%) create mode 100644 lib/classes/analytics/time_splitting/ten_percent_after_start.php diff --git a/analytics/classes/local/time_splitting/after_start.php b/analytics/classes/local/time_splitting/after_start.php new file mode 100644 index 0000000000000..ad3ebb8c926fb --- /dev/null +++ b/analytics/classes/local/time_splitting/after_start.php @@ -0,0 +1,111 @@ +. + +/** + * Time splitting method that generates predictions X days/weeks/months after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_analytics\local\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions X days/weeks/months after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class after_start extends \core_analytics\local\time_splitting\base implements before_now { + + /** + * The period we should wait until we generate predictions for this. + * + * @param \core_analytics\analysable $analysable + * @return \DateInterval + */ + abstract protected function wait_period(\core_analytics\analysable $analysable); + + /** + * Returns whether the course can be processed by this time splitting method or not. + * + * @param \core_analytics\analysable $analysable + * @return bool + */ + public function is_valid_analysable(\core_analytics\analysable $analysable) { + + if (!$analysable->get_start()) { + return false; + } + + $predictionstart = $this->get_prediction_interval_start($analysable); + if ($analysable->get_start() > $predictionstart) { + // We still need to wait. + return false; + } + + return true; + } + + /** + * This time-splitting method returns one single range, the start to two days before the end. + * + * @return array The list of ranges, each of them including 'start', 'end' and 'time' + */ + protected function define_ranges() { + + $now = time(); + $ranges = [ + [ + 'start' => $this->analysable->get_start(), + 'end' => $now, + 'time' => $now, + ] + ]; + + return $ranges; + } + + /** + * Whether to cache or not the indicator calculations. + * + * @return bool + */ + public function cache_indicator_calculations(): bool { + return false; + } + + /** + * Calculates the interval start time backwards, from now. + * + * @param \core_analytics\analysable $analysable + * @return int + */ + protected function get_prediction_interval_start(\core_analytics\analysable $analysable) { + + // The prediction time is always time(). We don't want to reuse the firstanalysis time + // because otherwise samples (e.g. students) which start after the analysable (e.g. course) + // start would use an incorrect analysis interval. + $predictionstart = new \DateTime('now'); + $predictionstart->sub($this->wait_period($analysable)); + + return $predictionstart->getTimestamp(); + } +} diff --git a/analytics/classes/local/time_splitting/past_periodic.php b/analytics/classes/local/time_splitting/past_periodic.php new file mode 100644 index 0000000000000..ee7b7d82c3dc4 --- /dev/null +++ b/analytics/classes/local/time_splitting/past_periodic.php @@ -0,0 +1,97 @@ +. + +/** + * Time splitting method that generates predictions regularly. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_analytics\local\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions periodically. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class past_periodic extends periodic implements before_now { + + /** + * Gets the next range with start on the provided time. + * + * The next range is based on the past period so we substract this + * range's periodicity from $time. + * + * @param \DateTimeImmutable $time + * @return array + */ + protected function get_next_range(\DateTimeImmutable $time) { + + $end = $time->getTimestamp(); + $start = $time->sub($this->periodicity())->getTimestamp(); + + if ($start < $this->analysable->get_start()) { + // We skip the first range generated as its start is prior to the analysable start. + return false; + } + + return [ + 'start' => $start, + 'end' => $end, + 'time' => $end + ]; + } + + /** + * Get the start of the first time range. + * + * @return int A timestamp. + */ + protected function get_first_start() { + return $this->analysable->get_start(); + } + + /** + * Guarantees that the last range dates end right now. + * + * @param array $ranges + * @return array + */ + protected function update_last_range(array $ranges) { + $lastrange = end($ranges); + + if ($lastrange['time'] > time()) { + // We just need to wait in this case. + return $lastrange; + } + + $timetoenddiff = time() - $lastrange['time']; + + $ranges[count($ranges) - 1] = [ + 'start' => $lastrange['start'] + $timetoenddiff, + 'end' => $lastrange['end'] + $timetoenddiff, + 'time' => $lastrange['time'] + $timetoenddiff, + ]; + + return $ranges; + } +} diff --git a/analytics/classes/local/time_splitting/periodic.php b/analytics/classes/local/time_splitting/periodic.php index 891820ea3c685..2fe0ef25f3efd 100644 --- a/analytics/classes/local/time_splitting/periodic.php +++ b/analytics/classes/local/time_splitting/periodic.php @@ -42,6 +42,21 @@ abstract class periodic extends base { */ abstract protected function periodicity(); + /** + * Gets the next range with start on the provided time. + * + * @param \DateTimeImmutable $time + * @return array + */ + abstract protected function get_next_range(\DateTimeImmutable $time); + + /** + * Get the start of the first time range. + * + * @return int A timestamp. + */ + abstract protected function get_first_start(); + /** * Returns whether the analysable can be processed by this time splitting method or not. * @@ -67,25 +82,42 @@ protected function define_ranges() { if ($this->analysable->get_end()) { $end = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_end()); } - $next = (new \DateTimeImmutable())->setTimestamp($this->get_first_start()); + $nexttime = (new \DateTimeImmutable())->setTimestamp($this->get_first_start()); $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object()); - $ranges = []; - while ($next < $now && - (empty($end) || $next < $end)) { - $range = $this->get_next_range($next); - if ($range) { - $ranges[] = $range; + $range = $this->get_next_range($nexttime); + if (!$range) { + $nexttime = $nexttime->add($periodicity); + $range = $this->get_next_range($nexttime); + + if (!$range) { + throw new \coding_exception('The get_next_range implementation is broken. The difference between two consecutive + ranges can not be more than the periodicity.'); } - $next = $next->add($periodicity); } - $nextrange = $this->get_next_range($next); - if ($this->ready_to_predict($nextrange) && (empty($end) || $next < $end)) { - // Add the next one if we have not reached the analysable end yet. - // It will be used to get predictions. - $ranges[] = $nextrange; + $ranges = []; + $endreached = false; + while (($this->ready_to_predict($range) || $this->ready_to_train($range)) && !$endreached) { + $ranges[] = $range; + $nexttime = $nexttime->add($periodicity); + $range = $this->get_next_range($nexttime); + + $endreached = (!empty($end) && $nexttime > $end); + } + + if ($ranges && !$endreached) { + // If this analysable is not finished we adjust the start and end of the last element in $ranges + // so that it ends in time().The reason is that the start of these ranges is based on the analysable + // start and the end is calculated based on the start. This is to prevent the same issue we had in MDL-65348. + // + // An example of the situation we want to avoid is: + // A course started on a Monday, in 2015. It has no end date. Now the system is upgraded to Moodle 3.8, which + // includes this code. This happens on Wednesday. Periodic ranges (e.g. weekly) will be calculated from a Monday + // so the data provided by the time-splitting method would be from Monday to Monday, when we really want to + // provide data from Wednesday to the past Wednesday. + $ranges = $this->update_last_range($ranges); } return $ranges; @@ -119,34 +151,12 @@ public function get_training_ranges() { } /** - * The next range is based on the past period. + * Allows child classes to update the last range provided. * - * @param \DateTimeImmutable $next + * @param array $ranges * @return array */ - protected function get_next_range(\DateTimeImmutable $next) { - - $end = $next->getTimestamp(); - $start = $next->sub($this->periodicity())->getTimestamp(); - - if ($start < $this->analysable->get_start()) { - // We skip the first range generated as its start is prior to the analysable start. - return false; - } - - return [ - 'start' => $start, - 'end' => $end, - 'time' => $end - ]; - } - - /** - * Get the start of the first time range. - * - * @return int A timestamp. - */ - protected function get_first_start() { - return $this->analysable->get_start(); + protected function update_last_range(array $ranges) { + return $ranges; } } diff --git a/analytics/classes/local/time_splitting/upcoming_periodic.php b/analytics/classes/local/time_splitting/upcoming_periodic.php index 7b3c9c6571a14..9d4e1dabd31ea 100644 --- a/analytics/classes/local/time_splitting/upcoming_periodic.php +++ b/analytics/classes/local/time_splitting/upcoming_periodic.php @@ -36,15 +36,18 @@ abstract class upcoming_periodic extends periodic implements after_now { /** - * The next range indicator calculations should be based on upcoming dates. + * Gets the next range with start on the provided time. * - * @param \DateTimeImmutable $next + * The next range is based on the upcoming period so we add this + * range's periodicity to $time. + * + * @param \DateTimeImmutable $time * @return array */ - protected function get_next_range(\DateTimeImmutable $next) { + protected function get_next_range(\DateTimeImmutable $time) { - $start = $next->getTimestamp(); - $end = $next->add($this->periodicity())->getTimestamp(); + $start = $time->getTimestamp(); + $end = $time->add($this->periodicity())->getTimestamp(); return [ 'start' => $start, 'end' => $end, @@ -87,7 +90,7 @@ protected function get_first_start() { return $firstanalysis; } - // This analysable has not yet been analysed, the start is therefore now (-1 so ready_to_predict can be executed). - return time() - 1; + // This analysable has not yet been analysed, the start is therefore now. + return time(); } } diff --git a/analytics/tests/fixtures/test_timesplitting_seconds.php b/analytics/tests/fixtures/test_timesplitting_seconds.php index e4b4f70423c67..289474bcc2049 100644 --- a/analytics/tests/fixtures/test_timesplitting_seconds.php +++ b/analytics/tests/fixtures/test_timesplitting_seconds.php @@ -31,7 +31,7 @@ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class test_timesplitting_seconds extends \core_analytics\local\time_splitting\periodic { +class test_timesplitting_seconds extends \core_analytics\local\time_splitting\past_periodic { /** * Every second. diff --git a/analytics/tests/manager_test.php b/analytics/tests/manager_test.php index 74e67216b636b..c092491f0ffa0 100644 --- a/analytics/tests/manager_test.php +++ b/analytics/tests/manager_test.php @@ -365,10 +365,14 @@ public function test_update_default_models_for_component() { $noteaching = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching'); $dropout = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'); $upcomingactivities = \core_analytics\manager::get_target('\core_user\analytics\target\upcoming_activities_due'); + $norecentaccesses = \core_analytics\manager::get_target('\core_course\analytics\target\no_recent_accesses'); + $noaccesssincestart = \core_analytics\manager::get_target('\core_course\analytics\target\no_access_since_course_start'); $this->assertTrue(\core_analytics\model::exists($noteaching)); $this->assertTrue(\core_analytics\model::exists($dropout)); $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); + $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); + $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); foreach (\core_analytics\manager::get_all_models() as $model) { $model->delete(); @@ -377,16 +381,22 @@ public function test_update_default_models_for_component() { $this->assertFalse(\core_analytics\model::exists($noteaching)); $this->assertFalse(\core_analytics\model::exists($dropout)); $this->assertFalse(\core_analytics\model::exists($upcomingactivities)); + $this->assertFalse(\core_analytics\model::exists($norecentaccesses)); + $this->assertFalse(\core_analytics\model::exists($noaccesssincestart)); $updated = \core_analytics\manager::update_default_models_for_component('moodle'); - $this->assertEquals(3, count($updated)); + $this->assertEquals(5, count($updated)); + $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); + $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(\core_analytics\model::exists($noteaching)); $this->assertTrue(\core_analytics\model::exists($dropout)); $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); + $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); + $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); $repeated = \core_analytics\manager::update_default_models_for_component('moodle'); diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php index 01bc9fda5fced..d543fa1c4a13b 100644 --- a/analytics/tests/stats_test.php +++ b/analytics/tests/stats_test.php @@ -53,7 +53,7 @@ public function test_enabled_models() { // By default, sites have {@link \core_course\analytics\target\no_teaching} and // {@link \core_user\analytics\target\upcoming_activities_due} enabled. - $this->assertEquals(2, \core_analytics\stats::enabled_models()); + $this->assertEquals(4, \core_analytics\stats::enabled_models()); $model = \core_analytics\model::create( \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'), @@ -63,11 +63,11 @@ public function test_enabled_models() { ); // Purely adding a new model does not make it included in the stats. - $this->assertEquals(2, \core_analytics\stats::enabled_models()); + $this->assertEquals(4, \core_analytics\stats::enabled_models()); // New models must be enabled to have them counted. $model->enable('\core\analytics\time_splitting\quarters'); - $this->assertEquals(3, \core_analytics\stats::enabled_models()); + $this->assertEquals(5, \core_analytics\stats::enabled_models()); } /** diff --git a/analytics/upgrade.txt b/analytics/upgrade.txt index 774b4fcc06be4..adb4b9cb7d436 100644 --- a/analytics/upgrade.txt +++ b/analytics/upgrade.txt @@ -12,6 +12,9 @@ information provided here is intended especially for developers. * Indicators can add information about calculated values by calling add_shared_calculation_info(). This data is later available for targets in get_insight_body_for_prediction(), it can be accessed appending ':extradata' to the indicator name (e.g. $sampledata['\mod_yeah\analytics\indicator\ou:extradata') +* A new \core_analytics\local\time_splitting\past_periodic abstract class has been added. Time-splitting + methods extending \core_analytics\local\time_splitting\periodic directly should be extending past_periodic + now. 'periodic' can still be directly extended by implementing get_next_range and get_first_start methods. === 3.7 === diff --git a/course/classes/analytics/target/no_access_since_course_start.php b/course/classes/analytics/target/no_access_since_course_start.php new file mode 100644 index 0000000000000..32c12af84bc4d --- /dev/null +++ b/course/classes/analytics/target/no_access_since_course_start.php @@ -0,0 +1,80 @@ +. + +/** + * No accesses since the start of the course. + * + * @package core_course + * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\analytics\target; + +defined('MOODLE_INTERNAL') || die(); + +/** + * No accesses since the start of the course. + * + * @package core_course + * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class no_access_since_course_start extends no_recent_accesses { + + /** + * Only past stuff whose start matches the course start. + * + * @param \core_analytics\local\time_splitting\base $timesplitting + * @return bool + */ + public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool { + return ($timesplitting instanceof \core_analytics\local\time_splitting\after_start); + } + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('target:noaccesssincecoursestart', 'course'); + } + + /** + * Returns the body message for the insight. + * + * @param \context $context + * @param string $contextname + * @param \stdClass $user + * @param \moodle_url $insighturl + * @return array The plain text message and the HTML message + */ + public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array { + global $OUTPUT; + + $a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname]; + $fullmessage = get_string('noaccesssincestartinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false); + $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message', + ['url' => $insighturl->out(false), 'insightinfomessage' => get_string('noaccesssincestartinfomessage', 'course', $a)] + ); + + return [$fullmessage, $fullmessagehtml]; + } + +} diff --git a/course/classes/analytics/target/no_recent_accesses.php b/course/classes/analytics/target/no_recent_accesses.php new file mode 100644 index 0000000000000..4f7626b74a350 --- /dev/null +++ b/course/classes/analytics/target/no_recent_accesses.php @@ -0,0 +1,135 @@ +. + +/** + * No recent accesses. + * + * @package core_course + * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\analytics\target; + +defined('MOODLE_INTERNAL') || die(); + +/** + * No recent accesses. + * + * @package core_course + * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class no_recent_accesses extends course_enrolments { + + /** + * Machine learning backends are not required to predict. + * + * @return bool + */ + public static function based_on_assumptions() { + return true; + } + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('target:norecentaccesses', 'course'); + } + + /** + * Returns the body message for the insight. + * + * @param \context $context + * @param string $contextname + * @param \stdClass $user + * @param \moodle_url $insighturl + * @return array The plain text message and the HTML message + */ + public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array { + global $OUTPUT; + + $a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname]; + $fullmessage = get_string('norecentaccessesinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false); + $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message', + ['url' => $insighturl->out(false), 'insightinfomessage' => get_string('norecentaccessesinfomessage', 'course', $a)] + ); + + return [$fullmessage, $fullmessagehtml]; + } + + /** + * Only past stuff whose start matches the course start. + * + * @param \core_analytics\local\time_splitting\base $timesplitting + * @return bool + */ + public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool { + return ($timesplitting instanceof \core_analytics\local\time_splitting\past_periodic); + } + + /** + * Discards courses that are not yet ready to be used for prediction. + * + * @param \core_analytics\analysable $course + * @param bool $fortraining + * @return true|string + */ + public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) { + + if (!$course->was_started()) { + return get_string('coursenotyetstarted', 'course'); + } + + if (!$this->students = $course->get_students()) { + return get_string('nocoursestudents', 'course'); + } + + if ($course->get_end() && $course->get_end() < $course->get_start()) { + return get_string('errorendbeforestart', 'course'); + } + + // Finished courses can not be used to get predictions. + if (!$fortraining && $course->is_finished()) { + return get_string('coursealreadyfinished', 'course'); + } + + return true; + } + + /** + * Do the user has any read action in the course? + * + * @param int $sampleid + * @param \core_analytics\analysable $analysable + * @param int $starttime + * @param int $endtime + * @return float 0 -> accesses, 1 -> no accesses. + */ + protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) { + + $readactions = $this->retrieve('\core\analytics\indicator\any_course_access', $sampleid); + if ($readactions == \core\analytics\indicator\any_course_access::get_min_value()) { + return 1; + } + return 0; + } +} diff --git a/lang/en/course.php b/lang/en/course.php index b73573555578b..636cf13050e5c 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -46,6 +46,12 @@ $string['nocourseendtime'] = 'The course does not have an end time'; $string['nocoursesections'] = 'No course sections'; $string['nocoursestudents'] = 'No students'; +$string['noaccesssincestartinfomessage'] = 'Hi {$a->userfirstname}, + +

Students in {$a->coursename} have never accessed the course.'; +$string['norecentaccessesinfomessage'] = 'Hi {$a->userfirstname}, + +

Students in {$a->coursename} have not accessed the course recently.'; $string['noteachinginfomessage'] = 'Hi {$a->userfirstname},

Courses with start dates in the next week have been identified as having no teacher or student enrolments.'; @@ -66,6 +72,10 @@ $string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.'; $string['target:coursegradetopass'] = 'Students at risk of not achieving the minimum grade to pass the course'; $string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not achieving the minimum grade to pass the course.'; +$string['target:noaccesssincecoursestart'] = 'Students who have not accessed the course yet'; +$string['target:noaccesssincecoursestart_help'] = 'This target describes students who never accessed a course they are enrolled in.'; +$string['target:norecentaccesses'] = 'Students who have not accessed the course recently'; +$string['target:norecentaccesses_help'] = 'This target describes students who have not accessed a course recently.'; $string['target:noteachingactivity'] = 'Courses at risk of not starting'; $string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.'; $string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 58180eecccafe..48edfc5d208c9 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1046,6 +1046,8 @@ $string['indicator:accessesbeforestart_help'] = 'This indicator reflects if the student accessed the course before the course start date.'; $string['indicator:activitiesdue'] = 'Activities due'; $string['indicator:activitiesdue_help'] = 'The user has activities due.'; +$string['indicator:anycourseaccess'] = 'Any course access'; +$string['indicator:anycourseaccess_help'] = 'This indicator reflects any accesses to the provided course for the provided user.'; $string['indicator:anywrite'] = 'Any write action'; $string['indicator:anywrite_help'] = 'This indicator represents any write (submit) action taken by the student.'; $string['indicator:anywriteincourse'] = 'Any write action in the course'; @@ -1991,12 +1993,24 @@ $string['timesplitting:quartersaccum_help'] = 'This analysis interval divides the course into quarters (4 equal parts), with each prediction being based on the accumulated data of all previous quarters.'; $string['timesplitting:singlerange'] = 'From start to end'; $string['timesplitting:singlerange_help'] = 'This analysis interval considers the entire course as a single span.'; +$string['timesplitting:onemonthafterstart'] = 'One month after start'; +$string['timesplitting:onemonthafterstart_help'] = 'This analysis interval generates a prediction 1 month after the analysable start.'; +$string['timesplitting:oneweekafterstart'] = 'One week after start'; +$string['timesplitting:oneweekafterstart_help'] = 'This analysis interval generates a prediction 1 week after the analysable start.'; +$string['timesplitting:past3days'] = 'Past 3 days'; +$string['timesplitting:past3days_help'] = 'This analysis interval generates predictions every 3 days. The indicators calculations will be based on the past 3 days.'; +$string['timesplitting:pastmonth'] = 'Past month'; +$string['timesplitting:pastmonth_help'] = 'This analysis interval generates predictions every month. The indicators calculations will be based on the past month.'; +$string['timesplitting:pastweek'] = 'Past week'; +$string['timesplitting:pastweek_help'] = 'This analysis interval generates predictions every week. The indicators calculations will be based on the past week.'; $string['timesplitting:upcoming3days'] = 'Upcoming 3 days'; $string['timesplitting:upcoming3days_help'] = 'This analysis interval generates predictions every 3 days. The indicators calculations will be based on the upcoming 3 days.'; $string['timesplitting:upcomingfortnight'] = 'Upcoming fortnight'; $string['timesplitting:upcomingfortnight_help'] = 'This analysis interval generates predictions every fortnight. The indicators calculations will be based on the upcoming fortnight.'; $string['timesplitting:upcomingweek'] = 'Upcoming week'; $string['timesplitting:upcomingweek_help'] = 'This analysis interval generates predictions every week. The indicators calculations will be based on the upcoming week.'; +$string['timesplitting:tenpercentafterstart'] = '10% after start'; +$string['timesplitting:tenpercentafterstart_help'] = 'This analysis interval generates a prediction after the 10% of the course is completed.'; $string['thanks'] = 'Thanks'; $string['theme'] = 'Theme'; $string['themes'] = 'Themes'; diff --git a/lib/classes/analytics/indicator/any_course_access.php b/lib/classes/analytics/indicator/any_course_access.php new file mode 100644 index 0000000000000..355e382dc0275 --- /dev/null +++ b/lib/classes/analytics/indicator/any_course_access.php @@ -0,0 +1,135 @@ +. + +/** + * Any access indicator. + * + * @package core + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Any access indicator. + * + * @package core + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class any_course_access extends \core_analytics\local\indicator\binary { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('indicator:anycourseaccess'); + } + + /** + * required_sample_data + * + * @return string[] + */ + public static function required_sample_data() { + return array('course', 'user'); + } + + /** + * Store userid => timeaccess relation if the provided analysable is a course. + * + * @param \core_analytics\analysable $analysable + * @return null + */ + public function fill_per_analysable_caches(\core_analytics\analysable $analysable) { + global $DB; + + if ($analysable instanceof \core_analytics\course) { + // Indexed by userid (there is a UNIQUE KEY at DB level). + $this->lastaccesses = $DB->get_records('user_lastaccess', ['courseid' => $analysable->get_id()], + '', 'userid, timeaccess'); + } + } + + /** + * calculate_sample + * + * @param int $sampleid + * @param string $sampleorigin + * @param int $starttime + * @param int $endtime + * @return float + */ + protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) { + + $course = $this->retrieve('course', $sampleid); + $user = $this->retrieve('user', $sampleid); + + // We first try using user_lastaccess as it is much faster than the log table. + if (empty($this->lastaccesses[$user->id]->timeaccess)) { + // The user never accessed. + return self::get_min_value(); + } else if (!$starttime && !$endtime) { + // No time restrictions, so all good as long as there is a record. + return self::get_max_value(); + } else if ($starttime && $this->lastaccesses[$user->id]->timeaccess < $starttime) { + // The last access is prior to $starttime. + return self::get_min_value(); + } else if ($endtime && $this->lastaccesses[$user->id]->timeaccess < $endtime) { + // The last access is before the $endtime. + return self::get_max_value(); + } else if ($starttime && !$endtime && $starttime <= $this->lastaccesses[$user->id]->timeaccess) { + // No end time, so max value as long as the last access is after $starttime. + return self::get_max_value(); + } + + // If the last access is after $endtime we can not know for sure if the user accessed or not + // between $starttime and $endtime, we need to check the logs table in this case. Note that + // it is unlikely that we will reach this point as this indicator will be used in models whose + // dates are in the past. + + if (!$logstore = \core_analytics\manager::get_analytics_logstore()) { + throw new \coding_exception('No available log stores'); + } + + // Filter by context to use the logstore_standard_log db table index. + $select = "userid = :userid AND courseid = :courseid"; + $params = ['courseid' => $course->id, 'userid' => $user->id]; + + if ($starttime) { + $select .= " AND timecreated > :starttime"; + $params['starttime'] = $starttime; + } + if ($endtime) { + $select .= " AND timecreated <= :endtime"; + $params['endtime'] = $endtime; + } + + $nlogs = $logstore->get_events_select_count($select, $params); + if ($nlogs) { + return self::get_max_value(); + } else { + return self::get_min_value(); + } + } +} diff --git a/lib/classes/analytics/time_splitting/one_month_after_start.php b/lib/classes/analytics/time_splitting/one_month_after_start.php new file mode 100644 index 0000000000000..650c176ab1384 --- /dev/null +++ b/lib/classes/analytics/time_splitting/one_month_after_start.php @@ -0,0 +1,56 @@ +. + +/** + * Time splitting method that generates predictions one month after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions one month after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class one_month_after_start extends \core_analytics\local\time_splitting\after_start { + + /** + * The time splitting method name. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('timesplitting:onemonthafterstart'); + } + + /** + * The period we should wait until we generate predictions for this. + * + * @param \core_analytics\analysable $analysable Not used in this implementation. + * @return \DateInterval + */ + protected function wait_period(\core_analytics\analysable $analysable) { + return new \DateInterval('P1M'); + } +} diff --git a/lib/classes/analytics/time_splitting/one_week_after_start.php b/lib/classes/analytics/time_splitting/one_week_after_start.php new file mode 100644 index 0000000000000..595748488b0f2 --- /dev/null +++ b/lib/classes/analytics/time_splitting/one_week_after_start.php @@ -0,0 +1,56 @@ +. + +/** + * Time splitting method that generates predictions one week after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions one week after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class one_week_after_start extends \core_analytics\local\time_splitting\after_start { + + /** + * The time splitting method name. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('timesplitting:oneweekafterstart'); + } + + /** + * The period we should wait until we generate predictions for this. + * + * @param \core_analytics\analysable $analysable Not used in this implementation. + * @return \DateInterval + */ + protected function wait_period(\core_analytics\analysable $analysable) { + return new \DateInterval('P1W'); + } +} diff --git a/lib/classes/analytics/time_splitting/past_3_days.php b/lib/classes/analytics/time_splitting/past_3_days.php new file mode 100644 index 0000000000000..a63a38a1417ff --- /dev/null +++ b/lib/classes/analytics/time_splitting/past_3_days.php @@ -0,0 +1,55 @@ +. + +/** + * Time splitting method that generates predictions every 3 days. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions every 3 days. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class past_3_days extends \core_analytics\local\time_splitting\past_periodic { + + /** + * The time splitting method name. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('timesplitting:past3days'); + } + + /** + * Once every 3 days. + * + * @return \DateInterval + */ + public function periodicity() { + return new \DateInterval('P3D'); + } +} diff --git a/lib/classes/analytics/time_splitting/past_month.php b/lib/classes/analytics/time_splitting/past_month.php new file mode 100644 index 0000000000000..3d73569eaf5a6 --- /dev/null +++ b/lib/classes/analytics/time_splitting/past_month.php @@ -0,0 +1,55 @@ +. + +/** + * Time splitting method that generates monthly predictions. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates monthly predictions. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class past_month extends \core_analytics\local\time_splitting\past_periodic { + + /** + * The time splitting method name. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('timesplitting:pastmonth'); + } + + /** + * Once a month. + * + * @return \DateInterval + */ + public function periodicity() { + return new \DateInterval('P1M'); + } +} diff --git a/analytics/tests/fixtures/test_timesplitting_weekly.php b/lib/classes/analytics/time_splitting/past_week.php similarity index 89% rename from analytics/tests/fixtures/test_timesplitting_weekly.php rename to lib/classes/analytics/time_splitting/past_week.php index 72403de9e0f87..438e5ac0e0a60 100644 --- a/analytics/tests/fixtures/test_timesplitting_weekly.php +++ b/lib/classes/analytics/time_splitting/past_week.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core\analytics\time_splitting; + defined('MOODLE_INTERNAL') || die(); /** @@ -31,18 +33,19 @@ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class test_timesplitting_weekly extends \core_analytics\local\time_splitting\periodic { +class past_week extends \core_analytics\local\time_splitting\past_periodic { /** * The time splitting method name. * @return \lang_string */ public static function get_name() : \lang_string { - return new \lang_string('error'); + return new \lang_string('timesplitting:pastweek'); } /** * Once per week. + * * @return \DateInterval */ public function periodicity() { diff --git a/lib/classes/analytics/time_splitting/ten_percent_after_start.php b/lib/classes/analytics/time_splitting/ten_percent_after_start.php new file mode 100644 index 0000000000000..a8b5642a6f41a --- /dev/null +++ b/lib/classes/analytics/time_splitting/ten_percent_after_start.php @@ -0,0 +1,81 @@ +. + +/** + * Time splitting method that generates predictions 3 days after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\time_splitting; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Time splitting method that generates predictions 3 days after the analysable start. + * + * @package core_analytics + * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ten_percent_after_start extends \core_analytics\local\time_splitting\after_start { + + /** + * The time splitting method name. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('timesplitting:tenpercentafterstart'); + } + + /** + * Extended as we require and end date here. + * + * @param \core_analytics\analysable $analysable + * @return bool + */ + public function is_valid_analysable(\core_analytics\analysable $analysable) { + + // We require an end date to calculate the 10%. + if (!$analysable->get_end()) { + return false; + } + + return parent::is_valid_analysable($analysable); + } + + /** + * The period we should wait until we generate predictions for this. + * + * @throws \coding_exception + * @param \core_analytics\analysable $analysable + * @return \DateInterval + */ + protected function wait_period(\core_analytics\analysable $analysable) { + + if (!$analysable->get_end() || !$analysable->get_start()) { + throw new \coding_exception('Analysables with no start or end should be discarded in is_valid_analysable.'); + } + + $diff = $analysable->get_end() - $analysable->get_start(); + + // A 10% of $diff. + return new \DateInterval('PT' . intval($diff / 10) . 'S'); + } +} diff --git a/lib/db/analytics.php b/lib/db/analytics.php index 5a689fcb250a9..7efac78d69456 100644 --- a/lib/db/analytics.php +++ b/lib/db/analytics.php @@ -97,4 +97,20 @@ 'timesplitting' => '\core\analytics\time_splitting\upcoming_week', 'enabled' => true, ], + [ + 'target' => '\core_course\analytics\target\no_access_since_course_start', + 'indicators' => [ + '\core\analytics\indicator\any_course_access', + ], + 'timesplitting' => '\core\analytics\time_splitting\one_month_after_start', + 'enabled' => true, + ], + [ + 'target' => '\core_course\analytics\target\no_recent_accesses', + 'indicators' => [ + '\core\analytics\indicator\any_course_access', + ], + 'timesplitting' => '\core\analytics\time_splitting\past_month', + 'enabled' => true, + ], ]; diff --git a/lib/tests/time_splittings_test.php b/lib/tests/time_splittings_test.php index 6394a3ed51c17..6587a70269b97 100644 --- a/lib/tests/time_splittings_test.php +++ b/lib/tests/time_splittings_test.php @@ -27,7 +27,6 @@ require_once(__DIR__ . '/../../analytics/tests/fixtures/test_timesplitting_seconds.php'); require_once(__DIR__ . '/../../analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php'); -require_once(__DIR__ . '/../../analytics/tests/fixtures/test_timesplitting_weekly.php'); require_once(__DIR__ . '/../../lib/enrollib.php'); /** @@ -197,23 +196,38 @@ public function test_periodic() { // Using a finished course. - $weekly = new test_timesplitting_weekly(); - $weekly->set_analysable($this->analysable); - $this->assertCount(1, $weekly->get_distinct_ranges()); + $pastweek = new \core\analytics\time_splitting\past_week(); + $pastweek->set_analysable($this->analysable); + $this->assertCount(1, $pastweek->get_distinct_ranges()); - $ranges = $weekly->get_all_ranges(); + $ranges = $pastweek->get_all_ranges(); $this->assertEquals(52, count($ranges)); $this->assertEquals($this->course->startdate, $ranges[0]['start']); $this->assertNotEquals($this->course->startdate, $ranges[0]['time']); // The analysable is finished so all ranges are available for training. - $this->assertCount(count($ranges), $weekly->get_training_ranges()); + $this->assertCount(count($ranges), $pastweek->get_training_ranges()); - $ranges = $weekly->get_most_recent_prediction_range(); + $ranges = $pastweek->get_most_recent_prediction_range(); $range = reset($ranges); $this->assertEquals(51, key($ranges)); - // We now use an ongoing course. + // We now use an ongoing course not yet ready to generate predictions. + + $threedaysago = new DateTime('-3 days'); + $params = array( + 'startdate' => $threedaysago->getTimestamp(), + ); + $ongoingcourse = $this->getDataGenerator()->create_course($params); + $ongoinganalysable = new \core_analytics\course($ongoingcourse); + + $pastweek = new \core\analytics\time_splitting\past_week(); + $pastweek->set_analysable($ongoinganalysable); + $ranges = $pastweek->get_all_ranges(); + $this->assertEquals(0, count($ranges)); + $this->assertCount(0, $pastweek->get_training_ranges()); + + // We now use a ready-to-predict ongoing course. $onemonthago = new DateTime('-30 days'); $params = array( @@ -222,20 +236,24 @@ public function test_periodic() { $ongoingcourse = $this->getDataGenerator()->create_course($params); $ongoinganalysable = new \core_analytics\course($ongoingcourse); - $weekly = new test_timesplitting_weekly(); - $weekly->set_analysable($ongoinganalysable); - $this->assertCount(1, $weekly->get_distinct_ranges()); + $pastweek = new \core\analytics\time_splitting\past_week(); + $pastweek->set_analysable($ongoinganalysable); + $this->assertCount(1, $pastweek->get_distinct_ranges()); - $ranges = $weekly->get_all_ranges(); + $ranges = $pastweek->get_all_ranges(); $this->assertEquals(4, count($ranges)); - $this->assertCount(4, $weekly->get_training_ranges()); + $this->assertCount(4, $pastweek->get_training_ranges()); - $ranges = $weekly->get_most_recent_prediction_range(); + $ranges = $pastweek->get_most_recent_prediction_range(); $range = reset($ranges); $this->assertEquals(3, key($ranges)); - $this->assertLessThan(time(), $range['time']); - $this->assertLessThan(time(), $range['start']); - $this->assertLessThan(time(), $range['end']); + $this->assertEquals(time(), $range['time'], '', 1); + // 1 second delta for the start just in case a second passes between the set_analysable call + // and this checking below. + $time = new \DateTime(); + $time->sub($pastweek->periodicity()); + $this->assertEquals($time->getTimestamp(), $range['start'], '', 1.0); + $this->assertEquals(time(), $range['end'], '', 1); $starttime = time(); @@ -246,8 +264,8 @@ public function test_periodic() { $ranges = $upcomingweek->get_all_ranges(); $this->assertEquals(1, count($ranges)); $range = reset($ranges); - $this->assertLessThan(time(), $range['time']); - $this->assertLessThan(time(), $range['start']); + $this->assertEquals(time(), $range['time'], '', 1); + $this->assertEquals(time(), $range['start'], '', 1); $this->assertGreaterThan(time(), $range['end']); $this->assertCount(0, $upcomingweek->get_training_ranges()); @@ -255,12 +273,10 @@ public function test_periodic() { $ranges = $upcomingweek->get_most_recent_prediction_range(); $range = reset($ranges); $this->assertEquals(0, key($ranges)); - $this->assertLessThan(time(), $range['time']); - $this->assertLessThan(time(), $range['start']); - // We substract 1 because upcoming_periodic also has that -1 so that predictions - // get executed once the first time range is set. - $this->assertGreaterThanOrEqual($starttime - 1, $range['time']); - $this->assertGreaterThanOrEqual($starttime - 1, $range['start']); + $this->assertEquals(time(), $range['time'], '', 1); + $this->assertEquals(time(), $range['start'], '', 1); + $this->assertGreaterThanOrEqual($starttime, $range['time']); + $this->assertGreaterThanOrEqual($starttime, $range['start']); $this->assertGreaterThan(time(), $range['end']); $this->assertNotEmpty($upcomingweek->get_range_by_index(0)); @@ -280,7 +296,8 @@ public function test_periodic() { $seconds->set_analysable($analysable); // Store the ranges we just obtained. - $nranges = count($seconds->get_all_ranges()); + $ranges = $seconds->get_all_ranges(); + $nranges = count($ranges); $ntrainingranges = count($seconds->get_training_ranges()); $mostrecentrange = $seconds->get_most_recent_prediction_range(); $mostrecentrange = reset($mostrecentrange); @@ -291,7 +308,8 @@ public function test_periodic() { // We set the analysable again so the time ranges are recalculated. $seconds->set_analysable($analysable); - $nnewranges = $seconds->get_all_ranges(); + $newranges = $seconds->get_all_ranges(); + $nnewranges = count($newranges); $nnewtrainingranges = $seconds->get_training_ranges(); $newmostrecentrange = $seconds->get_most_recent_prediction_range(); $newmostrecentrange = reset($newmostrecentrange); @@ -299,25 +317,67 @@ public function test_periodic() { $this->assertGreaterThan($ntrainingranges, $nnewtrainingranges); $this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']); - $seconds = new test_timesplitting_upcoming_seconds(); - $seconds->set_analysable($analysable); + // All the ranges but the last one should return the same values. + array_pop($ranges); + array_pop($newranges); + foreach ($ranges as $key => $range) { + $this->assertEquals($newranges[$key]['start'], $range['start']); + $this->assertEquals($newranges[$key]['end'], $range['end']); + $this->assertEquals($newranges[$key]['time'], $range['time']); + } + + // Fake model id, we can use any int, we will need to reference it later. + $modelid = 1505347200; + + $upcomingseconds = new test_timesplitting_upcoming_seconds(); + $upcomingseconds->set_modelid($modelid); + $upcomingseconds->set_analysable($analysable); // Store the ranges we just obtained. - $nranges = count($seconds->get_all_ranges()); - $ntrainingranges = count($seconds->get_training_ranges()); - $mostrecentrange = $seconds->get_most_recent_prediction_range(); + $ranges = $upcomingseconds->get_all_ranges(); + $nranges = count($ranges); + $ntrainingranges = count($upcomingseconds->get_training_ranges()); + $mostrecentrange = $upcomingseconds->get_most_recent_prediction_range(); $mostrecentrange = reset($mostrecentrange); + // Mimic the modelfirstanalyses caching in \core_analytics\analysis. + $this->mock_cache_first_analysis_caching($modelid, $analysable->get_id(), end($ranges)); + // We wait for the next range to be added. usleep(1000000); - $seconds->set_analysable($analysable); - $nnewranges = $seconds->get_all_ranges(); - $nnewtrainingranges = $seconds->get_training_ranges(); - $newmostrecentrange = $seconds->get_most_recent_prediction_range(); + // We set the analysable again so the time ranges are recalculated. + $upcomingseconds->set_analysable($analysable); + + $newranges = $upcomingseconds->get_all_ranges(); + $nnewranges = count($newranges); + $nnewtrainingranges = $upcomingseconds->get_training_ranges(); + $newmostrecentrange = $upcomingseconds->get_most_recent_prediction_range(); $newmostrecentrange = reset($newmostrecentrange); $this->assertGreaterThan($nranges, $nnewranges); $this->assertGreaterThan($ntrainingranges, $nnewtrainingranges); $this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']); + + // All the ranges but the last one should return the same values. + array_pop($ranges); + array_pop($newranges); + foreach ($ranges as $key => $range) { + $this->assertEquals($newranges[$key]['start'], $range['start']); + $this->assertEquals($newranges[$key]['end'], $range['end']); + $this->assertEquals($newranges[$key]['time'], $range['time']); + } + } + + /** + * Mocks core_analytics\analysis caching of the first time analysables were analysed. + * + * @param int $modelid + * @param int $analysableid + * @param array $range + * @return null + */ + private function mock_cache_first_analysis_caching($modelid, $analysableid, $range) { + $cache = \cache::make('core', 'modelfirstanalyses'); + $cache->set($modelid . '_' . $analysableid, $range['time']); } }